Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created November 13, 2023 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/b561b34bd8cba538a1eb16168b257684 to your computer and use it in GitHub Desktop.
Save bmeck/b561b34bd8cba538a1eb16168b257684 to your computer and use it in GitHub Desktop.
/// <reference types="node" />
// See NOTES below
import flatstr from 'flatstr'
import v8 from 'v8'
// @ts-check
class ArrayOfObjects {
store = []
constructor(count) {
for (let i = 0; i < count; i++) {
this.store.push({ [i]: true })
}
}
}
const n = +process.argv[2]
function alloc(name) {
return {
// NOTES:
// this should always be reaped in both
// - FIXME? it is held by JSON.stringify even though it need not be currently
// - gcstringify it is reaped but that has different semantics
//
// - if not an accessor the string table is blown out for userland during snapshot
// - this happens even when clearing `chunk` below and using `flatstr` oddly
// - (Stack roots) shows it is being held even when removing `root` ref in gcstringify
// - unclear how this is being held
get a() {
return new ArrayOfObjects(n || 1e5)
},
c: {
toJSON() {
if (name) v8.writeHeapSnapshot(name+'.heapsnapshot')
},
},
}
}
/**
* JSON.stringify but with the following change:
* - use iterators and eagerly grab entries() so that previous props can GC
*/
function gcstringify(root) {
let ret = ""
let chunk = ''
let stack = [
{
first: true,
pendingEntries: [["", root]],
tail: "",
},
]
root = null
while (stack.length) {
const currentNode = stack.slice(-1)[0]
if (currentNode.pendingEntries.length === 0) {
if (chunk.length >= 16 * 1024) {
ret += flatstr(chunk)
chunk = ''
}
chunk += currentNode.tail
stack.pop()
continue
}
const pendingEntry = currentNode.pendingEntries.shift()
let value = pendingEntry[1]
if (
(typeof value === "object" || typeof value === "bigint") &&
typeof value.toJSON === "function"
) {
value = value.toJSON(pendingEntry[0])
}
let valueStr = null
if (typeof value === "number") {
valueStr = JSON.stringify(value)
} else if (typeof value === "string") {
valueStr = JSON.stringify(value)
} else if (typeof value === "boolean") {
valueStr = value ? "true" : "false"
} else if (value === null) {
valueStr = "null"
} else if (typeof value === "object") {
if (Array.isArray(value)) {
valueStr = "["
stack.push({
first: true,
pendingEntries: Object.entries(value),
tail: "]",
})
} else {
valueStr = "{"
stack.push({
first: true,
pendingEntries: Object.entries(value),
tail: "}",
})
}
}
if (valueStr !== null) {
if (currentNode.first) {
currentNode.first = false
} else {
chunk += ","
}
if (currentNode.tail === "}") {
const key = pendingEntry[0]
chunk += `${JSON.stringify(key)}:`
}
chunk += valueStr
if (chunk.length >= 16 * 1024) {
ret += flatstr(chunk)
chunk = ''
}
}
}
return flatstr(ret + chunk)
}
if (JSON.stringify(alloc()) !== gcstringify(alloc())) {
throw new Error("mismatched serialization")
}
// fill up VM JIT state, MUST BE DONE AFTER BOTH gcstringify and JSON.stringify first run due to string interning
JSON.stringify(alloc("warmup"))
gcstringify(alloc("gcstringify"))
JSON.stringify(alloc("JSON.stringify"))
JSON.stringify(alloc("after"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment