-
Notifications
You must be signed in to change notification settings - Fork 35
Add Gc benchmark and Debugging #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
642eed3
c659979
e761888
14ae278
3cfbdb4
c79bf04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
// Use IntelliSense to learn about possible attributes. | ||
// Hover to view descriptions of existing attributes. | ||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||
"version": "0.2.0", | ||
"configurations": [ | ||
{ | ||
"name": "Test", | ||
"request": "launch", | ||
"runtimeArgs": ["run", "test"], | ||
"internalConsoleOptions": "openOnSessionStart", | ||
"runtimeExecutable": "pnpm", | ||
"skipFiles": ["<node_internals>/**"], | ||
"type": "node" | ||
}, | ||
{ | ||
"name": "Bench", | ||
"request": "launch", | ||
"runtimeArgs": ["run", "bench:debug"], | ||
"internalConsoleOptions": "openOnSessionStart", | ||
"runtimeExecutable": "pnpm", | ||
"skipFiles": ["<node_internals>/**"], | ||
"type": "node" | ||
} | ||
] | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import { | |
createMemo, | ||
createRoot, | ||
createSignal, | ||
} from "solid-js/dist/solid.cjs"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, how did you manage to resolve the browser version instead of the node version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updating type in package.json to module allows loading libraries as esm. And switching moduleResolution to bundler in tsconfig makes TS work with the change. |
||
} from "solid-js"; | ||
|
||
export const solidFramework: ReactiveFramework = { | ||
name: "SolidJS", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import v8 from "v8-natives"; | ||
import { fastestTest } from "./util/benchRepeat"; | ||
import { logPerfResult } from "./util/perfLogging"; | ||
import { Computed, ReactiveFramework } from "./util/reactiveFramework"; | ||
import { promiseDelay } from "./util/asyncUtil"; | ||
|
||
const cases = [ | ||
collectDynamicSources, | ||
collectDynamicSourcesAfterUpdate, | ||
collectOutOfScopeConsumers, | ||
]; | ||
|
||
export async function gcBench(framework: ReactiveFramework) { | ||
for (const c of cases) { | ||
const iter = framework.withBuild(() => c(framework)); | ||
|
||
const { timing } = await fastestTest(1, async () => { | ||
for (let i = 0; i < 1; i++) { | ||
await iter(); | ||
} | ||
}); | ||
|
||
logPerfResult({ | ||
framework: framework.name, | ||
test: `gc-${c.name}`, | ||
time: timing.time.toFixed(2), | ||
gcTime: timing.gcTime?.toFixed(2), | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Ensures old inactive dynamic sources get collected | ||
*/ | ||
function collectDynamicSourcesAfterUpdate(framework: ReactiveFramework) { | ||
const head = framework.signal(1); | ||
let dynamic = framework.signal(0); | ||
const tail = framework.computed(() => { | ||
return dynamic.read() + head.read(); | ||
}); | ||
|
||
const weakOldDynamic = new WeakRef(dynamic); | ||
|
||
// Create connection to first dynamic | ||
tail.read(); | ||
|
||
// Swap dynamic for new | ||
dynamic = framework.signal(1); | ||
|
||
// Trigger update to force invalidation of tail | ||
head.write(-1); | ||
|
||
return async () => { | ||
let didCollect = false; | ||
|
||
// Async GC loop to allow time for frameworks with scheduled cleanups | ||
for (let i = 0; i < 200; i++) { | ||
await promiseDelay(); | ||
v8.collectGarbage(); | ||
|
||
if (weakOldDynamic.deref() === undefined) { | ||
didCollect = true; | ||
break; | ||
} | ||
} | ||
|
||
console.assert(tail.read() === 0, "tail is not 0"); | ||
|
||
console.assert(didCollect, "Failed to collect inactive dynamic sources"); | ||
}; | ||
} | ||
|
||
/** | ||
* Ensures old inactive dynamic sources get collected | ||
*/ | ||
function collectDynamicSources(framework: ReactiveFramework) { | ||
const head = framework.signal(1); | ||
let dynamic = framework.signal(0); | ||
const tail = framework.computed(() => { | ||
return dynamic.read() + head.read(); | ||
}); | ||
|
||
const weakOldDynamic = new WeakRef(dynamic); | ||
|
||
// Create connection to first dynamic | ||
tail.read(); | ||
|
||
// Swap dynamic for new | ||
dynamic = framework.signal(1); | ||
|
||
return async () => { | ||
let didCollect = false; | ||
|
||
// Async GC loop to allow time for frameworks with scheduled cleanups | ||
for (let i = 0; i < 200; i++) { | ||
await promiseDelay(); | ||
v8.collectGarbage(); | ||
|
||
if (weakOldDynamic.deref() === undefined) { | ||
didCollect = true; | ||
break; | ||
} | ||
} | ||
|
||
console.assert(tail.read() === 1, "tail is not 1"); | ||
|
||
console.assert(didCollect, "Failed to collect inactive dynamic sources"); | ||
}; | ||
} | ||
|
||
/** | ||
* Ensures out of scope (computed) consumers get collected | ||
*/ | ||
function collectOutOfScopeConsumers(framework: ReactiveFramework) { | ||
const head = framework.signal(0); | ||
let tail: Computed<number> = head; | ||
const weakRefs: WeakRef<Computed<number>>[] = []; | ||
|
||
for (let i = 0; i < 5; i++) { | ||
const oldTail = tail; | ||
tail = framework.computed(() => { | ||
return oldTail.read() + 1; | ||
}); | ||
tail.read(); | ||
weakRefs.push(new WeakRef(tail)); | ||
} | ||
|
||
return async () => { | ||
let activeRefCount = 0; | ||
|
||
// Async GC loop to allow time for frameworks with scheduled cleanups | ||
for (let i = 0; i < 200; i++) { | ||
await promiseDelay(); | ||
v8.collectGarbage(); | ||
|
||
activeRefCount = 0; | ||
for (const ref of weakRefs) { | ||
if (ref.deref()) { | ||
activeRefCount++; | ||
} | ||
} | ||
if (activeRefCount === 0) { | ||
break; | ||
} | ||
} | ||
|
||
console.assert(head.read() === 0); | ||
|
||
console.assert( | ||
activeRefCount === 0, | ||
`Found ${activeRefCount} active references when there should be none` | ||
); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this loader?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes debugging much easier, as you can set breakpoints in the source files of this repo. I tried with esbuild building to dist folder with sourcemaps, which kindof works but the sourcemaps are incorrect leading to breakpoints not matching up correctly to the source, and when walking in the debugger it doesn't show the accurate line which is paused on.