diff --git a/README.md b/README.md
index df65ded278..5b8305e8d7 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,19 @@ npm run e2e
## Measures
+### TrackedEverything
+
+Records every source-observable value creation in the VS Code renderer from the
+first instrumented startup script through the end of the measured scenario.
+The result includes a lossless allocation-order stream and an interactive
+timeline city. This exhaustive instrumentation substantially changes runtime
+performance and memory use and does not include hidden V8 or Chromium
+allocations.
+
+```sh
+node packages/cli/bin/test.js --cwd packages/e2e --measure tracked-everything --only base
+```
+
### MemoryCity
Captures allocation-aware renderer and extension-host heap snapshots, computes
diff --git a/packages/charts/src/parts/GenerateCharts/GenerateCharts.ts b/packages/charts/src/parts/GenerateCharts/GenerateCharts.ts
index 2d1374b785..9f5dd1e498 100644
--- a/packages/charts/src/parts/GenerateCharts/GenerateCharts.ts
+++ b/packages/charts/src/parts/GenerateCharts/GenerateCharts.ts
@@ -160,4 +160,22 @@ export const generateCharts = async () => {
throw error
}
}
+
+ const trackedEverythingResults = join(Root.root, '.vscode-memory-leak-finder-results', 'tracked-everything')
+ try {
+ await access(trackedEverythingResults)
+ await execFileAsync(process.execPath, [
+ join(Root.root, 'packages', 'visualizations', 'src', 'generateTrackedEverything.ts'),
+ '--results',
+ trackedEverythingResults,
+ '--assets',
+ join(Root.root, 'packages', 'visualizations', 'dist'),
+ '--out',
+ join(Root.root, '.vscode-charts', 'tracked-everything'),
+ ])
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+ throw error
+ }
+ }
}
diff --git a/packages/charts/src/parts/GenerateIndexHtml/GenerateIndexHtml.ts b/packages/charts/src/parts/GenerateIndexHtml/GenerateIndexHtml.ts
index 2c0ea1b038..cefb77aacb 100644
--- a/packages/charts/src/parts/GenerateIndexHtml/GenerateIndexHtml.ts
+++ b/packages/charts/src/parts/GenerateIndexHtml/GenerateIndexHtml.ts
@@ -29,6 +29,11 @@ const baseStructure = `
const getMiddleHtml = (dirents: string[]) => {
let html = '
\n'
+ for (const directory of ['memory-city', 'tracked-everything']) {
+ if (dirents.includes(directory)) {
+ html += ` - Open ${directory.replaceAll('-', ' ')}
\n`
+ }
+ }
for (const dirent of dirents) {
if (!dirent.endsWith('.svg')) {
continue
diff --git a/packages/cli/src/parts/Cli/Cli.ts b/packages/cli/src/parts/Cli/Cli.ts
index c6b4df3331..4ef16394c8 100644
--- a/packages/cli/src/parts/Cli/Cli.ts
+++ b/packages/cli/src/parts/Cli/Cli.ts
@@ -15,6 +15,11 @@ export const run = async (platform: string, arch: string, argv: readonly string[
return
}
const options = ParseArgv.parseArgv(platform, arch, argv)
+ if (options.measure === 'tracked-everything' || options.measure === 'trackedEverything') {
+ console.warn(
+ 'Warning: tracked-everything exhaustively instruments source-observable creations and substantially changes runtime performance and memory use.',
+ )
+ }
// Parse isGithubActions once at startup
const isGithubActions = Boolean(env.GITHUB_ACTIONS)
diff --git a/packages/cli/src/parts/ParseArgv/ParseArgv.ts b/packages/cli/src/parts/ParseArgv/ParseArgv.ts
index 8ac0b2717a..3c46ad8c59 100644
--- a/packages/cli/src/parts/ParseArgv/ParseArgv.ts
+++ b/packages/cli/src/parts/ParseArgv/ParseArgv.ts
@@ -384,6 +384,8 @@ const parseTrackFunctions = (argv: readonly string[]): boolean => {
measure === 'trackedAllocationsFromStart' ||
measure === 'tracked-allocation-timeline' ||
measure === 'trackedAllocationTimeline' ||
+ measure === 'tracked-everything' ||
+ measure === 'trackedEverything' ||
measure === 'tracked-timeouts' ||
measure === 'trackedTimeouts'
)
diff --git a/packages/cli/test/ParseArgv.test.ts b/packages/cli/test/ParseArgv.test.ts
index 39f47ae9f8..185459a67b 100644
--- a/packages/cli/test/ParseArgv.test.ts
+++ b/packages/cli/test/ParseArgv.test.ts
@@ -597,6 +597,14 @@ test('parseArgv - tracked allocation timeline enables tracking transform', () =>
expect(options.trackFunctions).toBe(true)
})
+test('parseArgv - tracked everything enables tracking transform', () => {
+ for (const measure of ['tracked-everything', 'trackedEverything']) {
+ const options = ParseArgv.parseArgv('linux', 'x64', ['--measure', measure])
+ expect(options.measure).toBe(measure)
+ expect(options.trackFunctions).toBe(true)
+ }
+})
+
test('parseArgv - tracked timeouts enables tracking transform', () => {
const argv = ['--measure', 'tracked-timeouts']
const options = ParseArgv.parseArgv('linux', 'x64', argv)
diff --git a/packages/function-tracker/src/parts/CreateEverythingWrapperPlugin/CreateEverythingWrapperPlugin.ts b/packages/function-tracker/src/parts/CreateEverythingWrapperPlugin/CreateEverythingWrapperPlugin.ts
new file mode 100644
index 0000000000..0a42f072a7
--- /dev/null
+++ b/packages/function-tracker/src/parts/CreateEverythingWrapperPlugin/CreateEverythingWrapperPlugin.ts
@@ -0,0 +1,290 @@
+import type { NodePath, Visitor } from '@babel/core'
+import * as t from '@babel/types'
+
+export interface CreateEverythingWrapperPluginOptions {
+ readonly scriptId?: number | string
+}
+
+interface RestBinding {
+ readonly hint: 'Array' | 'Object'
+ readonly identifier: t.Identifier
+ readonly node: t.RestElement
+}
+
+const helperName = '__vscodeMemoryLeakFinderTrackEverything'
+const createHelper = (): t.MemberExpression => t.memberExpression(t.identifier('globalThis'), t.identifier(helperName))
+
+const getLocationNodes = (node: t.Node, scriptIdNode: t.Expression): readonly t.Expression[] => {
+ return [scriptIdNode, t.numericLiteral(node.loc?.start.line ?? -1), t.numericLiteral(node.loc?.start.column ?? -1)]
+}
+
+const getCalleeName = (node: t.Expression | t.V8IntrinsicIdentifier): string => {
+ if (t.isIdentifier(node)) {
+ return node.name
+ }
+ if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
+ return node.property.name
+ }
+ return 'Instance'
+}
+
+const isGeneratedHelperCall = (node: t.CallExpression | t.OptionalCallExpression): boolean => {
+ return (
+ t.isMemberExpression(node.callee) &&
+ t.isIdentifier(node.callee.object, { name: 'globalThis' }) &&
+ t.isIdentifier(node.callee.property, { name: helperName })
+ )
+}
+
+const shouldTrackLiteral = (path: NodePath): boolean => {
+ const parent = path.parentPath
+ if (!parent) {
+ return true
+ }
+ if (parent.isExpressionStatement() && Boolean((parent.node as t.ExpressionStatement & { directive?: string }).directive)) {
+ return false
+ }
+ if (
+ (parent.isObjectProperty() || parent.isObjectMethod() || parent.isClassProperty() || parent.isClassMethod()) &&
+ parent.node.key === path.node &&
+ !parent.node.computed
+ ) {
+ return false
+ }
+ if (parent.isMemberExpression() && parent.node.property === path.node && !parent.node.computed) {
+ return false
+ }
+ return !(
+ parent.isImportDeclaration() ||
+ parent.isExportAllDeclaration() ||
+ parent.isExportNamedDeclaration() ||
+ parent.isImportAttribute()
+ )
+}
+
+const getMethodLocations = (node: t.ObjectExpression | t.ClassExpression | t.ClassDeclaration): t.ArrayExpression => {
+ const methods: t.Node[] = t.isObjectExpression(node)
+ ? node.properties.filter((property) => t.isObjectMethod(property))
+ : node.body.body.filter((element) => t.isClassMethod(element) || t.isClassPrivateMethod(element))
+ return t.arrayExpression(
+ methods.map((method) =>
+ t.arrayExpression([t.numericLiteral(method.loc?.start.line ?? -1), t.numericLiteral(method.loc?.start.column ?? -1)]),
+ ),
+ )
+}
+
+const collectRestBindings = (node: t.Node | null | undefined, hint: 'Array' | 'Object', output: RestBinding[]): void => {
+ if (!node) {
+ return
+ }
+ if (t.isRestElement(node)) {
+ if (t.isIdentifier(node.argument)) {
+ output.push({ hint, identifier: node.argument, node })
+ } else {
+ collectRestBindings(node.argument, hint, output)
+ }
+ return
+ }
+ if (t.isObjectPattern(node)) {
+ for (const property of node.properties) {
+ if (t.isRestElement(property)) {
+ collectRestBindings(property, 'Object', output)
+ } else {
+ collectRestBindings(property.value, 'Object', output)
+ }
+ }
+ return
+ }
+ if (t.isArrayPattern(node)) {
+ for (const element of node.elements) {
+ collectRestBindings(element, 'Array', output)
+ }
+ return
+ }
+ if (t.isAssignmentPattern(node)) {
+ collectRestBindings(node.left, hint, output)
+ }
+}
+
+export const createEverythingWrapperPlugin = (options: CreateEverythingWrapperPluginOptions): Visitor => {
+ const { scriptId = 123 } = options
+ const scriptIdNode = typeof scriptId === 'number' ? t.numericLiteral(scriptId) : t.stringLiteral(scriptId)
+
+ const wrap = (
+ path: NodePath,
+ hint: string,
+ identityMode: 'created' | 'observed' = 'created',
+ methodLocations: t.ArrayExpression = t.arrayExpression(),
+ ): void => {
+ if (
+ path.findParent(
+ (parent) =>
+ (parent.isCallExpression() || parent.isOptionalCallExpression()) &&
+ isGeneratedHelperCall(parent.node as t.CallExpression | t.OptionalCallExpression),
+ )
+ ) {
+ return
+ }
+ const node = path.node
+ const wrapped = t.callExpression(createHelper(), [
+ node,
+ ...getLocationNodes(node, scriptIdNode),
+ t.stringLiteral(hint),
+ t.stringLiteral(identityMode),
+ methodLocations,
+ ])
+ path.replaceWith(wrapped)
+ path.skip()
+ }
+
+ const createRegistrationCall = (
+ value: t.Expression,
+ node: t.Node,
+ hint: string,
+ methods: t.ArrayExpression = t.arrayExpression(),
+ ): t.ExpressionStatement => {
+ return t.expressionStatement(
+ t.callExpression(createHelper(), [
+ value,
+ ...getLocationNodes(node, scriptIdNode),
+ t.stringLiteral(hint),
+ t.stringLiteral('created'),
+ methods,
+ ]),
+ )
+ }
+
+ const getRestRegistrations = (nodes: readonly (t.Node | null | undefined)[], defaultHint: 'Array' | 'Object') => {
+ const bindings: RestBinding[] = []
+ for (const node of nodes) {
+ collectRestBindings(node, defaultHint, bindings)
+ }
+ return bindings.map((binding) => createRegistrationCall(t.identifier(binding.identifier.name), binding.node, binding.hint))
+ }
+
+ const prependRegistrations = (body: t.BlockStatement, registrations: readonly t.ExpressionStatement[]): void => {
+ body.body.unshift(...registrations)
+ }
+
+ const registerRestParameter = (path: NodePath): void => {
+ const registrations = getRestRegistrations(path.node.params || [], 'Array')
+ if (registrations.length === 0) {
+ return
+ }
+ if (t.isBlockStatement(path.node.body)) {
+ prependRegistrations(path.node.body, registrations)
+ return
+ }
+ path.node.body = t.blockStatement([...registrations, t.returnStatement(path.node.body as t.Expression)])
+ }
+
+ const registerVariableRestBindings = (path: NodePath): void => {
+ const registrations = getRestRegistrations(
+ path.node.declarations.map((declaration) => declaration.id),
+ 'Array',
+ )
+ if (registrations.length === 0) {
+ return
+ }
+ if (path.parentPath?.isForInStatement() || path.parentPath?.isForOfStatement()) {
+ const loop = path.parentPath.node
+ if (!t.isBlockStatement(loop.body)) {
+ loop.body = t.blockStatement([loop.body])
+ }
+ prependRegistrations(loop.body, registrations)
+ return
+ }
+ if (path.parentPath?.isProgram() || path.parentPath?.isBlockStatement()) {
+ path.insertAfter(registrations)
+ }
+ }
+
+ const registerDeclaration = (path: NodePath, hint: string): void => {
+ const { id } = path.node
+ const { node } = path
+ const methods = t.isClassDeclaration(node) ? getMethodLocations(node) : t.arrayExpression()
+ const call = createRegistrationCall(id ? t.identifier(id.name) : t.unaryExpression('void', t.numericLiteral(0)), node, hint, methods)
+ path.insertAfter(call)
+ }
+
+ const literalExit = (path: NodePath, hint: string): void => {
+ if (shouldTrackLiteral(path)) {
+ wrap(path, hint)
+ }
+ }
+
+ return {
+ ArrayExpression: { exit: (path: NodePath) => wrap(path, 'Array') },
+ ArrowFunctionExpression: {
+ exit: (path: NodePath) => {
+ registerRestParameter(path)
+ wrap(path, 'Function')
+ },
+ },
+ BigIntLiteral: { exit: (path: NodePath) => literalExit(path, 'BigInt') },
+ BinaryExpression: { exit: (path: NodePath) => wrap(path, 'Dynamic') },
+ BooleanLiteral: { exit: (path: NodePath) => literalExit(path, 'Boolean') },
+ CallExpression: {
+ exit: (path: NodePath) => {
+ if (!isGeneratedHelperCall(path.node)) {
+ wrap(path, 'Dynamic', 'observed')
+ }
+ },
+ },
+ CatchClause: {
+ exit: (path: NodePath) => {
+ const registrations = getRestRegistrations([path.node.param], 'Object')
+ prependRegistrations(path.node.body, registrations)
+ },
+ },
+ ClassDeclaration: { exit: (path: NodePath) => registerDeclaration(path, 'Class') },
+ ClassExpression: {
+ exit: (path: NodePath) => wrap(path, 'Class', 'created', getMethodLocations(path.node)),
+ },
+ ClassMethod: { exit: (path: NodePath) => registerRestParameter(path) },
+ ClassPrivateMethod: { exit: (path: NodePath) => registerRestParameter(path) },
+ FunctionDeclaration: {
+ exit: (path: NodePath) => {
+ registerRestParameter(path)
+ registerDeclaration(path, 'Function')
+ },
+ },
+ FunctionExpression: {
+ exit: (path: NodePath) => {
+ registerRestParameter(path)
+ wrap(path, 'Function')
+ },
+ },
+ NewExpression: {
+ exit: (path: NodePath) => wrap(path, getCalleeName(path.node.callee)),
+ },
+ NullLiteral: { exit: (path: NodePath) => literalExit(path, 'Null') },
+ NumericLiteral: { exit: (path: NodePath) => literalExit(path, 'Number') },
+ ObjectExpression: {
+ exit: (path: NodePath) => wrap(path, 'Object', 'created', getMethodLocations(path.node)),
+ },
+ ObjectMethod: { exit: (path: NodePath) => registerRestParameter(path) },
+ OptionalCallExpression: {
+ exit: (path: NodePath) => {
+ if (!isGeneratedHelperCall(path.node)) {
+ wrap(path as NodePath, 'Dynamic', 'observed')
+ }
+ },
+ },
+ RegExpLiteral: { exit: (path: NodePath) => wrap(path, 'RegExp') },
+ StringLiteral: { exit: (path: NodePath) => literalExit(path, 'String') },
+ TaggedTemplateExpression: {
+ exit: (path: NodePath) => wrap(path, 'Dynamic', 'observed'),
+ },
+ TemplateLiteral: {
+ exit: (path: NodePath) => {
+ if (!path.parentPath?.isTaggedTemplateExpression()) {
+ wrap(path, 'String')
+ }
+ },
+ },
+ UnaryExpression: { exit: (path: NodePath) => wrap(path, 'Dynamic') },
+ UpdateExpression: { exit: (path: NodePath) => wrap(path, 'Number') },
+ VariableDeclaration: { exit: registerVariableRestBindings },
+ }
+}
diff --git a/packages/function-tracker/src/parts/PrepareTrackedVscode/PrepareTrackedVscode.ts b/packages/function-tracker/src/parts/PrepareTrackedVscode/PrepareTrackedVscode.ts
index 3515caf086..ef5d3b0b26 100644
--- a/packages/function-tracker/src/parts/PrepareTrackedVscode/PrepareTrackedVscode.ts
+++ b/packages/function-tracker/src/parts/PrepareTrackedVscode/PrepareTrackedVscode.ts
@@ -32,8 +32,10 @@ const transformerFiles = [
join(import.meta.dirname, '..', 'Transform', 'Transform.ts'),
join(import.meta.dirname, '..', 'TransformCodeWithTracking', 'TransformCodeWithTracking.ts'),
join(import.meta.dirname, '..', 'TransformCodeWithAllocationTracking', 'TransformCodeWithAllocationTracking.ts'),
+ join(import.meta.dirname, '..', 'TransformCodeWithEverythingTracking', 'TransformCodeWithEverythingTracking.ts'),
join(import.meta.dirname, '..', 'CreateFunctionWrapperPlugin', 'CreateFunctionWrapperPlugin.ts'),
join(import.meta.dirname, '..', 'CreateAllocationWrapperPlugin', 'CreateAllocationWrapperPlugin.ts'),
+ join(import.meta.dirname, '..', 'CreateEverythingWrapperPlugin', 'CreateEverythingWrapperPlugin.ts'),
]
const excludedDirectoryNames = new Set(['.build', '.cache', '.git', '.vscode-test'])
diff --git a/packages/function-tracker/src/parts/Transform/Transform.ts b/packages/function-tracker/src/parts/Transform/Transform.ts
index 8e6a499344..4c148182bb 100644
--- a/packages/function-tracker/src/parts/Transform/Transform.ts
+++ b/packages/function-tracker/src/parts/Transform/Transform.ts
@@ -1,5 +1,6 @@
import type { TransformOptions } from '../Types/Types.ts'
import { transformCodeWithAllocationTracking } from '../TransformCodeWithAllocationTracking/TransformCodeWithAllocationTracking.ts'
+import { transformCodeWithEverythingTracking } from '../TransformCodeWithEverythingTracking/TransformCodeWithEverythingTracking.ts'
import { transformCodeWithTracking } from '../TransformCodeWithTracking/TransformCodeWithTracking.ts'
const PREAMBLE_CODE = `(() => {
@@ -112,6 +113,137 @@ const ALLOCATION_PREAMBLE_CODE = `(() => {
})();
`
+const EVERYTHING_PREAMBLE_CODE = `(() => {
+ if(typeof globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingMetadata === 'function'){
+ return
+ }
+
+ const CHUNK_SIZE = 65536
+ const TIME_MARK_INTERVAL = 1024
+ const chunks = []
+ let currentChunk = new Uint32Array(CHUNK_SIZE)
+ let currentChunkLength = 0
+ let eventCount = 0
+ const sites = []
+ const siteIds = Object.create(null)
+ const seenIdentities = new WeakSet()
+ const seenSymbols = new Set()
+ const timeMarks = []
+ const now = () => typeof performance === 'object' && typeof performance.now === 'function' ? performance.now() : Date.now()
+ const startedAt = now()
+
+ const getType = (value, hint) => {
+ if(hint !== 'Dynamic'){
+ return hint
+ }
+ if(value === null){
+ return 'Null'
+ }
+ const valueType = typeof value
+ if(valueType === 'object'){
+ return Array.isArray(value) ? 'Array' : 'Object'
+ }
+ return valueType[0].toUpperCase() + valueType.slice(1)
+ }
+
+ const markTime = (force = false) => {
+ if(!force && eventCount !== 0 && eventCount % TIME_MARK_INTERVAL !== 0){
+ return
+ }
+ const elapsedMs = now() - startedAt
+ const previous = timeMarks[timeMarks.length - 1]
+ if(previous && previous.eventIndex === eventCount){
+ previous.elapsedMs = elapsedMs
+ return
+ }
+ timeMarks.push({ eventIndex: eventCount, elapsedMs })
+ }
+
+ const appendEvent = (siteId) => {
+ markTime()
+ if(currentChunkLength === CHUNK_SIZE){
+ chunks.push(currentChunk)
+ currentChunk = new Uint32Array(CHUNK_SIZE)
+ currentChunkLength = 0
+ }
+ currentChunk[currentChunkLength++] = siteId
+ eventCount++
+ }
+
+ const recordSite = (scriptId, line, column, type) => {
+ const location = \`\${scriptId}:\${line}:\${column}\`
+ const key = \`\${location}:\${type}\`
+ let siteId = siteIds[key]
+ if(siteId === undefined){
+ siteId = sites.length
+ siteIds[key] = siteId
+ sites.push({ id: siteId, location, type })
+ }
+ appendEvent(siteId)
+ }
+
+ const trackEverything = (value, scriptId, line, column, hint, identityMode, methodLocations) => {
+ const valueType = typeof value
+ if(value !== null && (valueType === 'object' || valueType === 'function')){
+ if(identityMode === 'observed' && seenIdentities.has(value)){
+ return value
+ }
+ seenIdentities.add(value)
+ } else if(valueType === 'symbol'){
+ if(identityMode === 'observed' && seenSymbols.has(value)){
+ return value
+ }
+ seenSymbols.add(value)
+ }
+ if(methodLocations.length > 0 && value !== null && (valueType === 'object' || valueType === 'function')){
+ const containers = hint === 'Class' ? [value, value.prototype] : [value]
+ for(const container of containers){
+ if(!container){
+ continue
+ }
+ for(const descriptor of Object.values(Object.getOwnPropertyDescriptors(container))){
+ if(typeof descriptor.value === 'function'){
+ seenIdentities.add(descriptor.value)
+ }
+ if(typeof descriptor.get === 'function'){
+ seenIdentities.add(descriptor.get)
+ }
+ if(typeof descriptor.set === 'function'){
+ seenIdentities.add(descriptor.set)
+ }
+ }
+ }
+ }
+ for(const methodLocation of methodLocations){
+ recordSite(scriptId, methodLocation[0], methodLocation[1], 'Function')
+ }
+ recordSite(scriptId, line, column, getType(value, hint))
+ return value
+ }
+
+ globalThis.__vscodeMemoryLeakFinderTrackEverything = trackEverything
+ globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingMetadata = () => {
+ markTime(true)
+ return {
+ chunkCount: chunks.length + (currentChunkLength > 0 ? 1 : 0),
+ durationMs: now() - startedAt,
+ eventCount,
+ sites,
+ timeMarks,
+ }
+ }
+ globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingChunk = (index) => {
+ if(index < chunks.length){
+ return Array.from(chunks[index])
+ }
+ if(index === chunks.length && currentChunkLength > 0){
+ return Array.from(currentChunk.subarray(0, currentChunkLength))
+ }
+ return []
+ }
+})();
+`
+
const TIMEOUT_PREAMBLE_CODE = `(() => {
if(typeof globalThis.getTrackedTimeoutCount === 'function'){
return
@@ -153,6 +285,10 @@ const TIMEOUT_PREAMBLE_CODE = `(() => {
export const transformCode = async (code: string, options: TransformOptions = {}): Promise => {
const { trackingMode = 'functions' } = options
+ if (trackingMode === 'everything') {
+ const transformedCode = transformCodeWithEverythingTracking(code, { ...options })
+ return EVERYTHING_PREAMBLE_CODE + '\n' + transformedCode
+ }
if (trackingMode === 'allocations') {
const transformedCode = transformCodeWithAllocationTracking(code, { ...options })
return ALLOCATION_PREAMBLE_CODE + '\n' + transformedCode
diff --git a/packages/function-tracker/src/parts/TransformCodeWithEverythingTracking/TransformCodeWithEverythingTracking.ts b/packages/function-tracker/src/parts/TransformCodeWithEverythingTracking/TransformCodeWithEverythingTracking.ts
new file mode 100644
index 0000000000..1e7c998148
--- /dev/null
+++ b/packages/function-tracker/src/parts/TransformCodeWithEverythingTracking/TransformCodeWithEverythingTracking.ts
@@ -0,0 +1,29 @@
+import { VError } from '@lvce-editor/verror'
+import type { TransformOptions } from '../Types/Types.ts'
+import { generate2, parser2, traverse2 } from '../BabelHelpers/BabelHelpers.ts'
+import { createEverythingWrapperPlugin } from '../CreateEverythingWrapperPlugin/CreateEverythingWrapperPlugin.ts'
+
+export const transformCodeWithEverythingTracking = (code: string, options: TransformOptions = {}): string => {
+ if (!code) {
+ return ''
+ }
+ const { filename = '', minify = false, scriptId = filename || 123 } = options
+ try {
+ const ast = parser2.parse(code, {
+ plugins: [],
+ ranges: false,
+ sourceType: 'module',
+ tokens: false,
+ })
+ traverse2(ast, createEverythingWrapperPlugin({ scriptId }))
+ return generate2(ast, {
+ comments: true,
+ compact: false,
+ jsonCompatibleStrings: false,
+ minified: minify,
+ retainLines: false,
+ }).code
+ } catch (error) {
+ throw new VError(error, `Error transforming code with everything tracking:`)
+ }
+}
diff --git a/packages/function-tracker/test/TransformCodeWithEverythingTracking.test.ts b/packages/function-tracker/test/TransformCodeWithEverythingTracking.test.ts
new file mode 100644
index 0000000000..d6ee985082
--- /dev/null
+++ b/packages/function-tracker/test/TransformCodeWithEverythingTracking.test.ts
@@ -0,0 +1,142 @@
+import { expect, test } from '@jest/globals'
+import { runInNewContext } from 'node:vm'
+import { transformCode } from '../src/parts/Transform/Transform.js'
+import { transformCodeWithEverythingTracking } from '../src/parts/TransformCodeWithEverythingTracking/TransformCodeWithEverythingTracking.js'
+
+const run = async (code: string) => {
+ const transformed = await transformCode(code, { scriptId: 7, trackingMode: 'everything' })
+ const context = { Date, Set, Uint32Array, WeakSet }
+ runInNewContext(transformed, context)
+ const metadata = (context as any).__vscodeMemoryLeakFinderGetTrackedEverythingMetadata()
+ const events = Array.from({ length: metadata.chunkCount }, (_, index) =>
+ (context as any).__vscodeMemoryLeakFinderGetTrackedEverythingChunk(index),
+ ).flat()
+ return { context: context as any, events, metadata, transformed }
+}
+
+test('tracks primitive evaluations and explicit identities in source order', async () => {
+ const { events, metadata } = await run(`
+ const number = 1 + 2
+ const string = \`value-\${number}\`
+ const array = []
+ const object = {}
+ globalThis.result = [number, string, array, object]
+ `)
+ const types = events.map((event: number) => metadata.sites[event].type)
+ expect(types).toEqual(['Number', 'Number', 'Number', 'String', 'Array', 'Object', 'Array'])
+})
+
+test('counts repeated primitive call results but only first observes an object identity', async () => {
+ const { events, metadata } = await run(`
+ const object = {}
+ const returnObject = () => object
+ const returnNumber = () => 1
+ returnObject()
+ returnObject()
+ returnNumber()
+ returnNumber()
+ `)
+ const types = events.map((event: number) => metadata.sites[event].type)
+ expect(types.filter((type: string) => type === 'Object')).toHaveLength(1)
+ expect(types.filter((type: string) => type === 'Number')).toHaveLength(4)
+ expect(events).toHaveLength(metadata.eventCount)
+ expect(metadata.sites.some((site: any) => site.type === 'Number')).toBe(true)
+})
+
+test('preserves directives, object keys, direct eval, optional calls, and results', async () => {
+ const { context, transformed } = await run(`
+ "use strict"
+ const value = { answer: 40 }
+ const fn = (amount) => amount + 2
+ globalThis.result = [eval('value.answer'), fn?.(40)]
+ `)
+ expect(context.result).toEqual([40, 42])
+ expect(transformed).toContain('"use strict"')
+ expect(transformed).toContain('answer:')
+})
+
+test('records function and class declarations and method creation', async () => {
+ const { events, metadata } = await run(`
+ function fn() {}
+ class Example { constructor() {} method() {} }
+ const instance = new Example()
+ const object = { method() {} }
+ `)
+ const types = events.map((event: number) => metadata.sites[event].type)
+ expect(types.filter((type: string) => type === 'Function')).toHaveLength(4)
+ expect(types).toContain('Class')
+ expect(types).toContain('Example')
+ expect(types).toContain('Object')
+ expect(metadata.sites.every((site: any) => !site.location.includes(':-1:-1'))).toBe(true)
+})
+
+test('does not instrument generated declaration metadata', () => {
+ const transformed = transformCodeWithEverythingTracking(`export function fn() {}\nexport class Example { method() {} }`, { scriptId: 7 })
+ expect(transformed).not.toContain('__vscodeMemoryLeakFinderTrackEverything(7, 7')
+ expect(transformed.match(/__vscodeMemoryLeakFinderTrackEverything/g)).toHaveLength(2)
+})
+
+test('counts anonymous default function and class declarations without renaming them', () => {
+ const functionCode = transformCodeWithEverythingTracking(`export default function () {}`, { scriptId: 7 })
+ const classCode = transformCodeWithEverythingTracking(`export default class { method() {} }`, { scriptId: 7 })
+ expect(functionCode).toContain('export default function () {}')
+ expect(functionCode).toContain('__vscodeMemoryLeakFinderTrackEverything(void 0, 7')
+ expect(classCode).toContain('export default class {')
+ expect(classCode).toContain('__vscodeMemoryLeakFinderTrackEverything(void 0, 7')
+})
+
+test('preserves tagged templates, inheritance, and super calls', async () => {
+ const { context } = await run(`
+ const tag = (parts, value) => parts[0] + value
+ class Base { constructor() { this.value = 2 } }
+ class Child extends Base { constructor() { super() } }
+ const child = new Child()
+ globalThis.result = tag\`value-\${child.value}\`
+ `)
+ expect(context.result).toBe('value-2')
+})
+
+test('tracks rest parameter arrays for functions, arrows, and methods', async () => {
+ const { context, events, metadata } = await run(`
+ function declared(...values) { return values.length }
+ const arrow = (...values) => values.length
+ const object = { method(...values) { return values.length } }
+ globalThis.result = [declared(1), arrow(1, 2), object.method(1, 2, 3)]
+ `)
+ expect(context.result).toEqual([1, 2, 3])
+ const types = events.map((event: number) => metadata.sites[event].type)
+ expect(types.filter((type: string) => type === 'Array').length).toBeGreaterThanOrEqual(4)
+})
+
+test('tracks array and object values created by destructuring rest', async () => {
+ const { context, events, metadata } = await run(`
+ const sourceObject = { keep: 1, value: 2 }
+ const sourceArray = [1, 2, 3]
+ const { keep, ...objectRest } = sourceObject
+ const [first, ...arrayRest] = sourceArray
+ const read = ({ keep, ...parameterRest }) => parameterRest.value
+ globalThis.result = [objectRest.value, arrayRest.length, read(sourceObject)]
+ `)
+ expect(context.result).toEqual([2, 2, 2])
+ const types = events.map((event: number) => metadata.sites[event].type)
+ expect(types.filter((type: string) => type === 'Object').length).toBeGreaterThanOrEqual(3)
+ expect(types.filter((type: string) => type === 'Array').length).toBeGreaterThanOrEqual(3)
+})
+
+test('uses fixed-size chunks without dropping events', async () => {
+ const { context } = await run(`globalThis.record = () => 1`)
+ for (let index = 0; index < 65_540; index++) {
+ context.record()
+ }
+ const metadata = context.__vscodeMemoryLeakFinderGetTrackedEverythingMetadata()
+ const total = Array.from({ length: metadata.chunkCount }, (_, index) =>
+ context.__vscodeMemoryLeakFinderGetTrackedEverythingChunk(index),
+ ).reduce((sum: number, chunk: readonly number[]) => sum + chunk.length, 0)
+ expect(total).toBe(metadata.eventCount)
+ expect(metadata.chunkCount).toBeGreaterThan(1)
+ expect(metadata.timeMarks.length).toBeGreaterThan(60)
+})
+
+test('empty input stays empty', () => {
+ expect(transformCodeWithEverythingTracking('')).toBe('')
+})
diff --git a/packages/launch-worker/src/parts/Launch/Launch.ts b/packages/launch-worker/src/parts/Launch/Launch.ts
index 6ab3dcb5d7..b5307eb231 100644
--- a/packages/launch-worker/src/parts/Launch/Launch.ts
+++ b/packages/launch-worker/src/parts/Launch/Launch.ts
@@ -47,6 +47,9 @@ export interface LaunchOptions {
let proxyWorkerRpc: any = null
export const getTrackingMode = (measureId: string): string => {
+ if (measureId === 'tracked-everything' || measureId === 'trackedEverything') {
+ return 'everything'
+ }
if (measureId === 'tracked-timeouts' || measureId === 'trackedTimeouts') {
return 'timeouts'
}
diff --git a/packages/launch-worker/test/Launch.test.ts b/packages/launch-worker/test/Launch.test.ts
index ea1282c669..5def161aad 100644
--- a/packages/launch-worker/test/Launch.test.ts
+++ b/packages/launch-worker/test/Launch.test.ts
@@ -15,6 +15,11 @@ test('getTrackingMode - tracked timeout measures use timeout tracking', () => {
expect(getTrackingMode('trackedTimeouts')).toBe('timeouts')
})
+test('getTrackingMode - tracked everything uses everything tracking', () => {
+ expect(getTrackingMode('tracked-everything')).toBe('everything')
+ expect(getTrackingMode('trackedEverything')).toBe('everything')
+})
+
test('getTrackingMode - other measures use function tracking', () => {
expect(getTrackingMode('tracked-functions')).toBe('functions')
expect(getTrackingMode('event-listener-count')).toBe('functions')
diff --git a/packages/memory-leak-finder/src/parts/CompareTrackedEverything/CompareTrackedEverything.ts b/packages/memory-leak-finder/src/parts/CompareTrackedEverything/CompareTrackedEverything.ts
new file mode 100644
index 0000000000..722a284f8f
--- /dev/null
+++ b/packages/memory-leak-finder/src/parts/CompareTrackedEverything/CompareTrackedEverything.ts
@@ -0,0 +1,68 @@
+import { basename, dirname, extname, join } from 'node:path'
+import { mkdir, rename, rm } from 'node:fs/promises'
+import type { Session } from '../Session/Session.ts'
+import type { ScriptMap } from '../ResolveTrackedLocationSourceMaps/ResolveTrackedLocationSourceMaps.ts'
+import * as ResolveTrackedLocationSourceMaps from '../ResolveTrackedLocationSourceMaps/ResolveTrackedLocationSourceMaps.ts'
+import type { TrackedEverythingMetadata, TrackedEverythingSite } from '../GetTrackedEverything/GetTrackedEverything.ts'
+
+export interface TrackedEverythingAfter {
+ readonly metadata: TrackedEverythingMetadata
+ readonly scriptMap: ScriptMap
+ readonly temporaryEventPath: string
+}
+
+export interface TrackedEverythingResultSite extends TrackedEverythingSite {
+ readonly originalColumn: number | null
+ readonly originalLine: number | null
+ readonly originalLocation: string | null
+ readonly originalSource: string | null
+}
+
+export interface TrackedEverythingResult {
+ readonly durationMs: number
+ readonly eventCount: number
+ readonly eventFile: string
+ readonly schemaVersion: 1
+ readonly sites: readonly TrackedEverythingResultSite[]
+ readonly timeMarks: TrackedEverythingMetadata['timeMarks']
+}
+
+const getEventPath = (resultPath: string): string => {
+ const extension = extname(resultPath)
+ const stem = extension ? basename(resultPath, extension) : basename(resultPath)
+ return join(dirname(resultPath), `${stem}.events.bin`)
+}
+
+export const compareTrackedEverything = async (
+ _before: unknown,
+ after: TrackedEverythingAfter,
+ context: Session & { readonly resultPath?: string },
+): Promise => {
+ if (!context.resultPath) {
+ throw new Error(`Tracked everything measure requires resultPath in compare context`)
+ }
+ const locations = after.metadata.sites.map((site) => site.location)
+ const resolved = await ResolveTrackedLocationSourceMaps.resolveTrackedLocationSourceMaps(locations, after.scriptMap)
+ const sites = after.metadata.sites.map((site) => {
+ const original = resolved[site.location]
+ return {
+ ...site,
+ originalColumn: original?.originalColumn ?? null,
+ originalLine: original?.originalLine ?? null,
+ originalLocation: original?.originalLocation ?? null,
+ originalSource: original?.originalSource ?? null,
+ }
+ })
+ const eventPath = getEventPath(context.resultPath)
+ await mkdir(dirname(eventPath), { recursive: true })
+ await rm(eventPath, { force: true })
+ await rename(after.temporaryEventPath, eventPath)
+ return {
+ durationMs: after.metadata.durationMs,
+ eventCount: after.metadata.eventCount,
+ eventFile: basename(eventPath),
+ schemaVersion: 1,
+ sites,
+ timeMarks: after.metadata.timeMarks,
+ }
+}
diff --git a/packages/memory-leak-finder/src/parts/GetTrackedEverything/GetTrackedEverything.ts b/packages/memory-leak-finder/src/parts/GetTrackedEverything/GetTrackedEverything.ts
new file mode 100644
index 0000000000..baac57f90a
--- /dev/null
+++ b/packages/memory-leak-finder/src/parts/GetTrackedEverything/GetTrackedEverything.ts
@@ -0,0 +1,67 @@
+import type { Session } from '../Session/Session.ts'
+import { DevtoolsProtocolRuntime } from '../DevtoolsProtocol/DevtoolsProtocol.ts'
+import { VError } from '../VError/VError.ts'
+
+export interface TrackedEverythingSite {
+ readonly id: number
+ readonly location: string
+ readonly type: string
+}
+
+export interface TrackedEverythingTimeMark {
+ readonly elapsedMs: number
+ readonly eventIndex: number
+}
+
+export interface TrackedEverythingMetadata {
+ readonly chunkCount: number
+ readonly durationMs: number
+ readonly eventCount: number
+ readonly sites: readonly TrackedEverythingSite[]
+ readonly timeMarks: readonly TrackedEverythingTimeMark[]
+}
+
+const emptyMetadata: TrackedEverythingMetadata = {
+ chunkCount: 0,
+ durationMs: 0,
+ eventCount: 0,
+ sites: [],
+ timeMarks: [],
+}
+
+export const getTrackedEverythingMetadata = async (session: Session): Promise => {
+ try {
+ const result = await DevtoolsProtocolRuntime.evaluate(session, {
+ expression: `(() => {
+ if (typeof globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingMetadata === 'function') {
+ return globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingMetadata()
+ }
+ return null
+ })()`,
+ returnByValue: true,
+ })
+ if (!result || typeof result !== 'object') {
+ return emptyMetadata
+ }
+ return result as TrackedEverythingMetadata
+ } catch (error) {
+ throw new VError(error, `Failed to get tracked-everything metadata`)
+ }
+}
+
+export const getTrackedEverythingChunk = async (session: Session, index: number): Promise => {
+ try {
+ const result = await DevtoolsProtocolRuntime.evaluate(session, {
+ expression: `(() => {
+ if (typeof globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingChunk === 'function') {
+ return globalThis.__vscodeMemoryLeakFinderGetTrackedEverythingChunk(${index})
+ }
+ return []
+ })()`,
+ returnByValue: true,
+ })
+ return Array.isArray(result) ? result : []
+ } catch (error) {
+ throw new VError(error, `Failed to get tracked-everything event chunk ${index}`)
+ }
+}
diff --git a/packages/memory-leak-finder/src/parts/MeasureId/MeasureId.ts b/packages/memory-leak-finder/src/parts/MeasureId/MeasureId.ts
index 9ae91aad90..ad59c79198 100644
--- a/packages/memory-leak-finder/src/parts/MeasureId/MeasureId.ts
+++ b/packages/memory-leak-finder/src/parts/MeasureId/MeasureId.ts
@@ -133,6 +133,7 @@ export const TextEncoderCount = 'textEncoderCount'
export const TrackedAllocationTimeline = 'trackedAllocationTimeline'
export const TrackedAllocations = 'trackedAllocations'
export const TrackedAllocationsFromStart = 'trackedAllocationsFromStart'
+export const TrackedEverything = 'trackedEverything'
export const TrackedFunctions = 'trackedFunctions'
export const TrackedTimeouts = 'trackedTimeouts'
export const TrustedTypePolicyCount = 'trustedTypePolicyCount'
diff --git a/packages/memory-leak-finder/src/parts/MeasureTrackedEverything/MeasureTrackedEverything.ts b/packages/memory-leak-finder/src/parts/MeasureTrackedEverything/MeasureTrackedEverything.ts
new file mode 100644
index 0000000000..ce5656cd98
--- /dev/null
+++ b/packages/memory-leak-finder/src/parts/MeasureTrackedEverything/MeasureTrackedEverything.ts
@@ -0,0 +1,87 @@
+import { randomUUID } from 'node:crypto'
+import { mkdir, open, rm } from 'node:fs/promises'
+import { dirname, join } from 'node:path'
+import type { IScriptHandler } from '../IScriptHandler/IScriptHandler.ts'
+import type { Session } from '../Session/Session.ts'
+import * as CompareTrackedEverything from '../CompareTrackedEverything/CompareTrackedEverything.ts'
+import * as GetTrackedEverything from '../GetTrackedEverything/GetTrackedEverything.ts'
+import * as MeasureId from '../MeasureId/MeasureId.ts'
+import * as Root from '../Root/Root.ts'
+import * as ScriptHandler from '../ScriptHandler/ScriptHandler.ts'
+import * as TargetId from '../TargetId/TargetId.ts'
+
+interface TrackedEverythingState {
+ readonly scriptHandler: IScriptHandler
+ readonly temporaryEventPath: string
+}
+
+export const id = MeasureId.TrackedEverything
+export const targets = [TargetId.Browser]
+
+export const create = (session: Session): readonly [Session, TrackedEverythingState] => {
+ return [
+ session,
+ {
+ scriptHandler: ScriptHandler.create(),
+ temporaryEventPath: join(Root.root, '.vscode-tracked-everything', `${randomUUID()}.events.bin`),
+ },
+ ]
+}
+
+export const start = async (session: Session, state: TrackedEverythingState) => {
+ await state.scriptHandler.start(session)
+ return {}
+}
+
+const writeChunk = async (handle: Awaited>, chunk: readonly number[]): Promise => {
+ const buffer = Buffer.allocUnsafe(chunk.length * 4)
+ for (let index = 0; index < chunk.length; index++) {
+ buffer.writeUInt32LE(chunk[index], index * 4)
+ }
+ await handle.write(buffer)
+}
+
+export const stop = async (session: Session, state: TrackedEverythingState) => {
+ const metadata = await GetTrackedEverything.getTrackedEverythingMetadata(session)
+ if (metadata.eventCount === 0) {
+ await state.scriptHandler.stop(session)
+ throw new Error(
+ 'Tracked everything produced no data. The VS Code workbench was not instrumented, or no instrumented modules were loaded.',
+ )
+ }
+ await mkdir(dirname(state.temporaryEventPath), { recursive: true })
+ const handle = await open(state.temporaryEventPath, 'w')
+ let writtenEventCount = 0
+ try {
+ for (let index = 0; index < metadata.chunkCount; index++) {
+ const chunk = await GetTrackedEverything.getTrackedEverythingChunk(session, index)
+ await writeChunk(handle, chunk)
+ writtenEventCount += chunk.length
+ }
+ } finally {
+ await handle.close()
+ await state.scriptHandler.stop(session)
+ }
+ if (writtenEventCount !== metadata.eventCount) {
+ await rm(state.temporaryEventPath, { force: true })
+ throw new Error(`Tracked everything event stream is incomplete: expected ${metadata.eventCount}, received ${writtenEventCount}`)
+ }
+ return {
+ metadata,
+ scriptMap: state.scriptHandler.scriptMap,
+ temporaryEventPath: state.temporaryEventPath,
+ }
+}
+
+export const compare = CompareTrackedEverything.compareTrackedEverything
+
+export const isLeak = () => false
+
+export const releaseResources = async (session: Session, state: TrackedEverythingState): Promise => {
+ try {
+ await state.scriptHandler.stop(session)
+ } catch {
+ // Debugger may already be disabled.
+ }
+ await rm(state.temporaryEventPath, { force: true })
+}
diff --git a/packages/memory-leak-finder/src/parts/Measures/Measures.ts b/packages/memory-leak-finder/src/parts/Measures/Measures.ts
index 38066b9865..34df629dca 100644
--- a/packages/memory-leak-finder/src/parts/Measures/Measures.ts
+++ b/packages/memory-leak-finder/src/parts/Measures/Measures.ts
@@ -129,6 +129,7 @@ export * as MeasureTextEncoderCount from '../MeasureTextEncoderCount/MeasureText
export * as MeasureTrackedAllocationTimeline from '../MeasureTrackedAllocationTimeline/MeasureTrackedAllocationTimeline.ts'
export * as MeasureTrackedAllocations from '../MeasureTrackedAllocations/MeasureTrackedAllocations.ts'
export * as MeasureTrackedAllocationsFromStart from '../MeasureTrackedAllocationsFromStart/MeasureTrackedAllocationsFromStart.ts'
+export * as MeasureTrackedEverything from '../MeasureTrackedEverything/MeasureTrackedEverything.ts'
export * as MeasureTrackedFunctions from '../MeasureTrackedFunctions/MeasureTrackedFunctions.ts'
export * as MeasureTrackedTimeouts from '../MeasureTrackedTimeouts/MeasureTrackedTimeouts.ts'
export * as MeasureTrustedTypePolicyCount from '../MeasureTrustedTypePolicyCount/MeasureTrustedTypePolicyCount.ts'
diff --git a/packages/memory-leak-finder/test/GetMeasure.test.ts b/packages/memory-leak-finder/test/GetMeasure.test.ts
index 96356b2a2d..edf4e45a3e 100644
--- a/packages/memory-leak-finder/test/GetMeasure.test.ts
+++ b/packages/memory-leak-finder/test/GetMeasure.test.ts
@@ -52,6 +52,19 @@ test('getMeasure resolves tracked allocation timeline public measure id', () =>
expect(GetMeasure.getMeasure(MemoryLeakFinder, 'tracked-allocation-timeline')).toBe(measure)
})
+test('getMeasure resolves tracked everything public measure id', () => {
+ const measure = {
+ id: 'trackedEverything',
+ }
+ const MemoryLeakFinder = {
+ Measures: {
+ MeasureTrackedEverything: measure,
+ },
+ }
+ expect(GetMeasure.getMeasure(MemoryLeakFinder, 'tracked-everything')).toBe(measure)
+ expect(GetMeasure.getMeasure(MemoryLeakFinder, 'trackedEverything')).toBe(measure)
+})
+
test('getMeasure resolves tracked timeouts public measure id', () => {
const measure = {
id: 'trackedTimeouts',
diff --git a/packages/memory-leak-finder/test/MeasureTrackedEverything.test.ts b/packages/memory-leak-finder/test/MeasureTrackedEverything.test.ts
new file mode 100644
index 0000000000..89e1ede823
--- /dev/null
+++ b/packages/memory-leak-finder/test/MeasureTrackedEverything.test.ts
@@ -0,0 +1,106 @@
+import { expect, jest, test } from '@jest/globals'
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+
+const mockGetMetadata = jest.fn<() => Promise>()
+const mockGetChunk = jest.fn<(_session: any, index: number) => Promise>()
+
+jest.unstable_mockModule('../src/parts/GetTrackedEverything/GetTrackedEverything.ts', () => ({
+ getTrackedEverythingChunk: mockGetChunk,
+ getTrackedEverythingMetadata: mockGetMetadata,
+}))
+
+const MeasureTrackedEverything = await import('../src/parts/MeasureTrackedEverything/MeasureTrackedEverything.ts')
+const CompareTrackedEverything = await import('../src/parts/CompareTrackedEverything/CompareTrackedEverything.ts')
+
+test('tracked everything preserves startup data and writes chunks in order', async () => {
+ const directory = await mkdtemp(join(tmpdir(), 'tracked-everything-measure-'))
+ const eventPath = join(directory, 'events.bin')
+ const start = jest.fn<(_session: any) => Promise>().mockResolvedValue()
+ const stop = jest.fn<(_session: any) => Promise>().mockResolvedValue()
+ const state = {
+ scriptHandler: { scriptMap: {}, start, stop },
+ temporaryEventPath: eventPath,
+ }
+ const session = {} as any
+ mockGetMetadata.mockResolvedValue({
+ chunkCount: 2,
+ durationMs: 12,
+ eventCount: 3,
+ sites: [{ id: 0, location: '7:1:2', type: 'Object' }],
+ timeMarks: [{ elapsedMs: 0, eventIndex: 0 }],
+ })
+ mockGetChunk.mockImplementation(async (_session, index) => (index === 0 ? [4, 2] : [9]))
+
+ await expect(MeasureTrackedEverything.start(session, state)).resolves.toEqual({})
+ expect(mockGetMetadata).not.toHaveBeenCalled()
+ const result = await MeasureTrackedEverything.stop(session, state)
+
+ expect(start).toHaveBeenCalledWith(session)
+ expect(stop).toHaveBeenCalledWith(session)
+ expect(result.metadata.eventCount).toBe(3)
+ const bytes = await readFile(eventPath)
+ expect([bytes.readUInt32LE(0), bytes.readUInt32LE(4), bytes.readUInt32LE(8)]).toEqual([4, 2, 9])
+ await rm(directory, { force: true, recursive: true })
+})
+
+test('tracked everything rejects empty instrumentation output', async () => {
+ const stop = jest.fn<(_session: any) => Promise>().mockResolvedValue()
+ mockGetMetadata.mockResolvedValue({ chunkCount: 0, durationMs: 0, eventCount: 0, sites: [], timeMarks: [] })
+ await expect(
+ MeasureTrackedEverything.stop({} as any, {
+ scriptHandler: { scriptMap: {}, start: jest.fn<(_session: any) => Promise>().mockResolvedValue(), stop },
+ temporaryEventPath: '/unused',
+ }),
+ ).rejects.toThrow('Tracked everything produced no data')
+ expect(stop).toHaveBeenCalled()
+})
+
+test('tracked everything comparison publishes the binary sidecar and versioned metadata', async () => {
+ const directory = await mkdtemp(join(tmpdir(), 'tracked-everything-compare-'))
+ const temporaryEventPath = join(directory, 'temporary.bin')
+ const resultPath = join(directory, 'scenario.json')
+ await writeFile(temporaryEventPath, Buffer.from([0, 0, 0, 0]))
+ const result = await CompareTrackedEverything.compareTrackedEverything(
+ {},
+ {
+ metadata: {
+ chunkCount: 1,
+ durationMs: 42,
+ eventCount: 1,
+ sites: [{ id: 0, location: '7:1:2', type: 'Object' }],
+ timeMarks: [
+ { elapsedMs: 0, eventIndex: 0 },
+ { elapsedMs: 42, eventIndex: 1 },
+ ],
+ },
+ scriptMap: {},
+ temporaryEventPath,
+ },
+ { resultPath } as any,
+ )
+ expect(result).toEqual({
+ durationMs: 42,
+ eventCount: 1,
+ eventFile: 'scenario.events.bin',
+ schemaVersion: 1,
+ sites: [
+ {
+ id: 0,
+ location: '7:1:2',
+ originalColumn: null,
+ originalLine: null,
+ originalLocation: null,
+ originalSource: null,
+ type: 'Object',
+ },
+ ],
+ timeMarks: [
+ { elapsedMs: 0, eventIndex: 0 },
+ { elapsedMs: 42, eventIndex: 1 },
+ ],
+ })
+ expect(await readFile(join(directory, result.eventFile))).toEqual(Buffer.from([0, 0, 0, 0]))
+ await rm(directory, { force: true, recursive: true })
+})
diff --git a/packages/visualizations/src/MemoryCityScene.tsx b/packages/visualizations/src/MemoryCityScene.tsx
index e7abdfc975..656074c6fc 100644
--- a/packages/visualizations/src/MemoryCityScene.tsx
+++ b/packages/visualizations/src/MemoryCityScene.tsx
@@ -133,6 +133,7 @@ const createScene = (container: HTMLDivElement): SceneState => {
mesh.castShadow = true
mesh.receiveShadow = true
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
+ mesh.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 12, 0), 100)
scene.add(mesh)
const districts = new THREE.Group()
@@ -173,7 +174,9 @@ const createScene = (container: HTMLDivElement): SceneState => {
export const MemoryCityScene = ({ buildings, districts, onFailure, onHover, onSelect, resetToken, selectedPath }: MemoryCitySceneProps) => {
const containerRef = useRef(null)
+ const callbacksRef = useRef({ onFailure, onHover, onSelect })
const stateRef = useRef<(SceneState & { camera: THREE.PerspectiveCamera; dispose: () => void }) | null>(null)
+ callbacksRef.current = { onFailure, onHover, onSelect }
useEffect(() => {
const container = containerRef.current
@@ -184,7 +187,7 @@ export const MemoryCityScene = ({ buildings, districts, onFailure, onHover, onSe
stateRef.current = createScene(container) as SceneState & { camera: THREE.PerspectiveCamera; dispose: () => void }
} catch (error) {
console.error('Failed to initialize Memory City WebGL renderer', error)
- onFailure()
+ callbacksRef.current.onFailure()
return
}
const state = stateRef.current
@@ -201,13 +204,13 @@ export const MemoryCityScene = ({ buildings, districts, onFailure, onHover, onSe
const handleMove = (event: PointerEvent): void => {
const building = getHit(event)
state.renderer.domElement.style.cursor = building ? 'pointer' : 'grab'
- onHover(building, event.clientX, event.clientY)
+ callbacksRef.current.onHover(building, event.clientX, event.clientY)
}
- const handleLeave = (): void => onHover(null, 0, 0)
+ const handleLeave = (): void => callbacksRef.current.onHover(null, 0, 0)
const handleClick = (event: PointerEvent): void => {
const building = getHit(event)
if (building) {
- onSelect(building)
+ callbacksRef.current.onSelect(building)
}
}
state.renderer.domElement.addEventListener('pointermove', handleMove)
@@ -220,7 +223,7 @@ export const MemoryCityScene = ({ buildings, districts, onFailure, onHover, onSe
state.dispose()
stateRef.current = null
}
- }, [onFailure, onHover, onSelect])
+ }, [])
useEffect(() => {
const state = stateRef.current
diff --git a/packages/visualizations/src/TrackedEverythingApp.tsx b/packages/visualizations/src/TrackedEverythingApp.tsx
new file mode 100644
index 0000000000..3a31a625ca
--- /dev/null
+++ b/packages/visualizations/src/TrackedEverythingApp.tsx
@@ -0,0 +1,371 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { ChevronRight, Layers3, Pause, Play, Search } from 'lucide-react'
+import { computeCityLayout, getGrowthColor } from './layout.ts'
+import { MemoryCityScene } from './MemoryCityScene.tsx'
+import { TrackedEverythingTimeline } from './TrackedEverythingTimeline.tsx'
+import { decodeTrackedEverythingEvents, eventIndexToTime, getSitePath, timeToEventIndex } from './trackedEverythingModel.ts'
+import type { TrackedEverythingAggregates, TrackedEverythingDataset } from './trackedEverythingTypes.ts'
+import type { BuildingLayout, BuildingView } from './types.ts'
+
+const emptyAggregates: TrackedEverythingAggregates = {
+ cursor: 0,
+ fileCounts: {},
+ recentFileCounts: {},
+ recentSiteCounts: [],
+ siteCounts: [],
+ timeline: {},
+ typeCounts: {},
+ types: [],
+}
+
+const getFinalCounts = (
+ events: Uint32Array,
+ dataset: TrackedEverythingDataset,
+): { readonly fileCounts: Record; readonly siteCounts: number[] } => {
+ const fileCounts: Record = Object.create(null)
+ const siteCounts = Array.from({ length: dataset.sites.length }, () => 0)
+ for (const siteId of events) {
+ const path = getSitePath(dataset.sites[siteId])
+ fileCounts[path] = (fileCounts[path] || 0) + 1
+ siteCounts[siteId]++
+ }
+ return { fileCounts, siteCounts }
+}
+
+const toView = (path: string, count: number): BuildingView => ({
+ deltaBytes: count,
+ growthPercent: count === 0 ? 0 : 100,
+ growthSlope: count,
+ kind: 'source',
+ largestObjectRetainedBytes: count,
+ objectCount: count,
+ path,
+ retainedBytes: count,
+ shallowBytes: count,
+})
+
+const getSiteBuildingPath = (siteId: number): string => `site/${siteId}`
+
+export const TrackedEverythingApp = ({ dataset }: { readonly dataset: TrackedEverythingDataset }) => {
+ const workerRef = useRef(null)
+ const [aggregates, setAggregates] = useState(emptyAggregates)
+ const [axis, setAxis] = useState<'allocation' | 'time'>('time')
+ const [error, setError] = useState('')
+ const [finalFileCounts, setFinalFileCounts] = useState>({})
+ const [finalSiteCounts, setFinalSiteCounts] = useState([])
+ const [playing, setPlaying] = useState(false)
+ const [query, setQuery] = useState('')
+ const [selectedPath, setSelectedPath] = useState('')
+ const [selectedSiteId, setSelectedSiteId] = useState(null)
+ const [selectedType, setSelectedType] = useState('')
+
+ useEffect(() => {
+ const worker = new Worker(new URL('./trackedEverythingWorker.ts', import.meta.url), { type: 'module' })
+ workerRef.current = worker
+ worker.onmessage = (event: MessageEvent) => {
+ setAggregates(event.data)
+ }
+ const load = async (): Promise => {
+ try {
+ const response = await fetch(dataset.eventFile)
+ if (!response.ok) {
+ throw new Error(`event stream request returned ${response.status}`)
+ }
+ const buffer = await response.arrayBuffer()
+ if (buffer.byteLength !== dataset.eventCount * 4) {
+ throw new Error(`expected ${dataset.eventCount * 4} event bytes, received ${buffer.byteLength}`)
+ }
+ const events = decodeTrackedEverythingEvents(buffer, dataset)
+ const finalCounts = getFinalCounts(events, dataset)
+ setFinalFileCounts(finalCounts.fileCounts)
+ setFinalSiteCounts(finalCounts.siteCounts)
+ const normalizedBuffer = events.buffer as ArrayBuffer
+ worker.postMessage({ buffer: normalizedBuffer, dataset, kind: 'init' }, [normalizedBuffer])
+ } catch (loadError) {
+ setError(loadError instanceof Error ? loadError.message : String(loadError))
+ }
+ }
+ void load()
+ return () => {
+ worker.terminate()
+ workerRef.current = null
+ }
+ }, [dataset])
+
+ const setCursor = useCallback(
+ (cursor: number, type = selectedType) => {
+ workerRef.current?.postMessage({ cursor, kind: 'cursor', selectedType: type })
+ },
+ [selectedType],
+ )
+
+ useEffect(() => {
+ if (!playing || aggregates.cursor >= dataset.eventCount) {
+ return
+ }
+ const increment = Math.max(1, Math.ceil(dataset.eventCount / 240))
+ const handle = window.setInterval(() => {
+ const next = Math.min(dataset.eventCount, aggregates.cursor + increment)
+ setCursor(next)
+ if (next === dataset.eventCount) {
+ setPlaying(false)
+ }
+ }, 50)
+ return () => window.clearInterval(handle)
+ }, [aggregates.cursor, dataset.eventCount, playing, setCursor])
+
+ const visibleSites = useMemo(
+ () =>
+ selectedPath
+ ? dataset.sites.filter((site) => getSitePath(site) === selectedPath && (!selectedType || site.type === selectedType))
+ : [],
+ [dataset.sites, selectedPath, selectedType],
+ )
+ const finalViews = useMemo(
+ () =>
+ selectedPath
+ ? visibleSites.map((site) => toView(getSiteBuildingPath(site.id), finalSiteCounts[site.id] || 0))
+ : Object.entries(finalFileCounts).map(([path, count]) => toView(path, count)),
+ [finalFileCounts, finalSiteCounts, selectedPath, visibleSites],
+ )
+ const finalLayout = useMemo(() => computeCityLayout(finalViews), [finalViews])
+ const buildings = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase()
+ const recentLimit = Math.max(
+ 1,
+ ...(selectedPath
+ ? visibleSites.map((site) => aggregates.recentSiteCounts[site.id] || 0)
+ : Object.values(aggregates.recentFileCounts)),
+ )
+ return finalLayout.buildings
+ .filter((building) => {
+ if (!normalizedQuery) {
+ return true
+ }
+ if (!selectedPath) {
+ return building.path.toLowerCase().includes(normalizedQuery)
+ }
+ const siteId = Number(building.path.split('/').at(-1))
+ const site = dataset.sites[siteId]
+ return `${site.type} ${site.originalSource || site.location} ${site.originalLine || ''}`.toLowerCase().includes(normalizedQuery)
+ })
+ .map((building): BuildingLayout => {
+ const siteId = selectedPath ? Number(building.path.split('/').at(-1)) : -1
+ const current = selectedPath ? aggregates.siteCounts[siteId] || 0 : aggregates.fileCounts[building.path] || 0
+ const final = selectedPath ? finalSiteCounts[siteId] || 1 : finalFileCounts[building.path] || 1
+ const recent = selectedPath ? aggregates.recentSiteCounts[siteId] || 0 : aggregates.recentFileCounts[building.path] || 0
+ const progress = Math.log1p(current) / Math.log1p(final)
+ return {
+ ...building,
+ color: getGrowthColor(recent, recentLimit),
+ deltaBytes: current,
+ growthPercent: (current / final) * 100,
+ growthSlope: recent,
+ height: current === 0 ? 0.001 : 0.001 + (building.height - 0.001) * progress,
+ objectCount: current,
+ retainedBytes: current,
+ shallowBytes: current,
+ }
+ })
+ }, [
+ aggregates.fileCounts,
+ aggregates.recentFileCounts,
+ aggregates.recentSiteCounts,
+ aggregates.siteCounts,
+ dataset.sites,
+ finalFileCounts,
+ finalLayout.buildings,
+ finalSiteCounts,
+ query,
+ selectedPath,
+ visibleSites,
+ ])
+ const selectedSites = useMemo(
+ () =>
+ visibleSites
+ .map((site) => ({
+ ...site,
+ count: aggregates.siteCounts[site.id] || 0,
+ recentCount: aggregates.recentSiteCounts[site.id] || 0,
+ }))
+ .filter((site) => site.count > 0)
+ .toSorted((left, right) => right.count - left.count)
+ .slice(0, 12),
+ [aggregates.recentSiteCounts, aggregates.siteCounts, visibleSites],
+ )
+ const selectedSite = selectedSiteId === null ? undefined : dataset.sites[selectedSiteId]
+ const elapsedMs = eventIndexToTime(dataset.timeMarks, aggregates.cursor, dataset.durationMs)
+ const sliderValue = axis === 'time' ? elapsedMs : aggregates.cursor
+ const sliderMax = axis === 'time' ? dataset.durationMs : dataset.eventCount
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedPath && (
+ <>
+
+ {selectedPath}
+ >
+ )}
+
+
+
+ setError('WebGL could not initialize')}
+ onHover={() => {}}
+ onSelect={(building) => {
+ if (selectedPath) {
+ setSelectedSiteId(Number(building.path.split('/').at(-1)))
+ } else {
+ setSelectedPath(building.path)
+ setSelectedSiteId(null)
+ }
+ }}
+ resetToken={0}
+ selectedPath={selectedSiteId === null ? '' : getSiteBuildingPath(selectedSiteId)}
+ />
+
+
+ {error && {error}
}
+
+
+
+ )
+}
diff --git a/packages/visualizations/src/TrackedEverythingTimeline.tsx b/packages/visualizations/src/TrackedEverythingTimeline.tsx
new file mode 100644
index 0000000000..6f6909f3ca
--- /dev/null
+++ b/packages/visualizations/src/TrackedEverythingTimeline.tsx
@@ -0,0 +1,53 @@
+import { useEffect, useRef } from 'react'
+
+const colors = ['#ee48b1', '#24d5de', '#8b7cf6', '#f5b84b', '#63d471', '#f4775b', '#5aa8ff', '#c4cfdf']
+
+export const TrackedEverythingTimeline = ({
+ cursor,
+ eventCount,
+ timeline,
+ types,
+}: {
+ readonly cursor: number
+ readonly eventCount: number
+ readonly timeline: Readonly>
+ readonly types: readonly string[]
+}) => {
+ const ref = useRef(null)
+ useEffect(() => {
+ const canvas = ref.current
+ if (!canvas) {
+ return
+ }
+ const width = canvas.clientWidth
+ const height = canvas.clientHeight
+ const scale = window.devicePixelRatio || 1
+ canvas.width = Math.round(width * scale)
+ canvas.height = Math.round(height * scale)
+ const context = canvas.getContext('2d')
+ if (!context) {
+ return
+ }
+ context.scale(scale, scale)
+ context.clearRect(0, 0, width, height)
+ const binCount = types.length ? timeline[types[0]]?.length || 0 : 0
+ const totals = Array.from({ length: binCount }, (_, bin) => types.reduce((total, type) => total + (timeline[type]?.[bin] || 0), 0))
+ const maximum = Math.max(1, ...totals)
+ for (let bin = 0; bin < binCount; bin++) {
+ const x = (bin / Math.max(1, binCount)) * width
+ const binWidth = Math.ceil(width / Math.max(1, binCount)) + 1
+ let bottom = height
+ for (let typeIndex = 0; typeIndex < types.length; typeIndex++) {
+ const value = timeline[types[typeIndex]]?.[bin] || 0
+ const segmentHeight = (value / maximum) * height
+ context.fillStyle = colors[typeIndex % colors.length]
+ context.fillRect(x, bottom - segmentHeight, binWidth, segmentHeight)
+ bottom -= segmentHeight
+ }
+ }
+ const cursorX = (cursor / Math.max(1, eventCount)) * width
+ context.fillStyle = '#ffffff'
+ context.fillRect(cursorX - 1, 0, 2, height)
+ }, [cursor, eventCount, timeline, types])
+ return
+}
diff --git a/packages/visualizations/src/generateTrackedEverything.ts b/packages/visualizations/src/generateTrackedEverything.ts
new file mode 100644
index 0000000000..ba76ba6f3a
--- /dev/null
+++ b/packages/visualizations/src/generateTrackedEverything.ts
@@ -0,0 +1,106 @@
+import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
+import { basename, dirname, join, relative, resolve } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+interface RawTrackedEverythingResult {
+ readonly durationMs: number
+ readonly eventCount: number
+ readonly eventFile: string
+ readonly schemaVersion: 1
+ readonly sites: readonly unknown[]
+ readonly timeMarks: readonly unknown[]
+}
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+const repositoryRoot = resolve(packageRoot, '..', '..')
+
+const parseArguments = (argv: readonly string[]) => {
+ let assets = resolve(packageRoot, 'dist')
+ let out = resolve(repositoryRoot, '.vscode-charts', 'tracked-everything')
+ let results = resolve(repositoryRoot, '.vscode-memory-leak-finder-results', 'tracked-everything')
+ for (let index = 0; index < argv.length; index++) {
+ if (argv[index] === '--assets') {
+ assets = resolve(argv[++index])
+ } else if (argv[index] === '--out') {
+ out = resolve(argv[++index])
+ } else if (argv[index] === '--results') {
+ results = resolve(argv[++index])
+ }
+ }
+ return { assets, out, results }
+}
+
+const findResults = async (root: string): Promise => {
+ const output: string[] = []
+ const visit = async (path: string): Promise => {
+ for (const entry of await readdir(path, { withFileTypes: true })) {
+ const child = join(path, entry.name)
+ if (entry.isDirectory()) {
+ await visit(child)
+ } else if (entry.name.endsWith('.json')) {
+ const value = JSON.parse(await readFile(child, 'utf8'))
+ if (
+ value?.schemaVersion === 1 &&
+ typeof value?.eventFile === 'string' &&
+ Number.isInteger(value?.eventCount) &&
+ Array.isArray(value?.sites)
+ ) {
+ output.push(child)
+ }
+ }
+ }
+ }
+ await visit(root)
+ return output.toSorted()
+}
+
+const writeViewer = async (resultPath: string, resultsRoot: string, assets: string, outputRoot: string): Promise => {
+ const result = JSON.parse(await readFile(resultPath, 'utf8')) as RawTrackedEverythingResult
+ const relativeResult = relative(resultsRoot, resultPath)
+ const outputPath = join(outputRoot, relativeResult.replace(/\.json$/, ''))
+ const eventSource = join(dirname(resultPath), result.eventFile)
+ const eventFile = basename(result.eventFile)
+ const dataset = {
+ ...result,
+ eventFile,
+ kind: 'tracked-everything',
+ scenario: relativeResult.replace(/\.json$/, '').replaceAll('\\', '/'),
+ }
+ await mkdir(outputPath, { recursive: true })
+ await cp(assets, outputPath, { force: true, recursive: true })
+ await cp(eventSource, join(outputPath, eventFile), { force: true })
+ await writeFile(join(outputPath, 'tracked-everything.json'), `${JSON.stringify(dataset, null, 2)}\n`)
+ await writeFile(
+ join(outputPath, 'tracked-everything-data.js'),
+ `globalThis.__TRACKED_EVERYTHING_DATA__ = ${JSON.stringify(dataset).replaceAll('<', '\\u003c')}\n`,
+ )
+ const indexPath = join(outputPath, 'index.html')
+ const html = (await readFile(indexPath, 'utf8'))
+ .replace('VS Code Memory City', 'VS Code Tracked Everything')
+ .replace('./memory-city-data.js', './tracked-everything-data.js')
+ await writeFile(indexPath, html)
+ return outputPath
+}
+
+export const generateTrackedEverything = async (results: string, assets: string, out: string): Promise => {
+ const resultPaths = await findResults(results)
+ const outputs: string[] = []
+ for (const resultPath of resultPaths) {
+ outputs.push(await writeViewer(resultPath, results, assets, out))
+ }
+ await mkdir(out, { recursive: true })
+ const links = outputs
+ .map((output) => `- ${relative(out, output)}
`)
+ .join('\n')
+ await writeFile(
+ join(out, 'index.html'),
+ `Tracked EverythingTracked Everything
`,
+ )
+ return outputs
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
+ const { assets, out, results } = parseArguments(process.argv.slice(2))
+ const outputs = await generateTrackedEverything(results, assets, out)
+ console.log(`Generated ${outputs.length} tracked-everything visualization(s) at ${out}`)
+}
diff --git a/packages/visualizations/src/main.tsx b/packages/visualizations/src/main.tsx
index 1d1deebcb2..de3d8bc048 100644
--- a/packages/visualizations/src/main.tsx
+++ b/packages/visualizations/src/main.tsx
@@ -1,16 +1,23 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App.tsx'
+import { TrackedEverythingApp } from './TrackedEverythingApp.tsx'
import { isMemoryCityDataset } from './model.ts'
import { sampleData } from './sampleData.ts'
+import { isTrackedEverythingDataset } from './trackedEverythingModel.ts'
+import type { TrackedEverythingDataset } from './trackedEverythingTypes.ts'
import type { MemoryCityDataset } from './types.ts'
import './styles.css'
declare global {
var __MEMORY_CITY_DATA__: unknown
+ var __TRACKED_EVERYTHING_DATA__: unknown
}
-const loadDataset = async (): Promise<{ dataset: MemoryCityDataset; loadError?: string }> => {
+const loadDataset = async (): Promise<{ dataset: MemoryCityDataset | TrackedEverythingDataset; loadError?: string }> => {
+ if (isTrackedEverythingDataset(globalThis.__TRACKED_EVERYTHING_DATA__)) {
+ return { dataset: globalThis.__TRACKED_EVERYTHING_DATA__ }
+ }
if (isMemoryCityDataset(globalThis.__MEMORY_CITY_DATA__)) {
return { dataset: globalThis.__MEMORY_CITY_DATA__ }
}
@@ -42,6 +49,6 @@ if (!root) {
}
createRoot(root).render(
-
+ {isTrackedEverythingDataset(dataset) ? : }
,
)
diff --git a/packages/visualizations/src/styles.css b/packages/visualizations/src/styles.css
index b4edf854b2..4b94a9046f 100644
--- a/packages/visualizations/src/styles.css
+++ b/packages/visualizations/src/styles.css
@@ -447,6 +447,105 @@ h1 {
box-shadow: 0 -14px 50px rgb(0 0 0 / 20%);
}
+.EverythingApp .Viewport {
+ min-height: 0;
+}
+
+.TypeSelect {
+ background: #101b2e;
+ border: 1px solid #29405f;
+ border-radius: 7px;
+ color: #dce9fa;
+ min-width: 145px;
+ padding: 8px 10px;
+}
+
+.EverythingPath {
+ align-items: center;
+ color: #7891b2;
+ display: flex;
+ font-size: 12px;
+ gap: 5px;
+ min-height: 18px;
+}
+
+.EverythingPath button {
+ background: none;
+ border: 0;
+ color: #a5bddb;
+ cursor: pointer;
+ font: inherit;
+ padding: 0;
+}
+
+.EverythingInspector {
+ max-height: calc(100% - 42px);
+ overflow: auto;
+ width: min(360px, 34vw);
+}
+
+.EverythingInspector ol {
+ list-style: none;
+ margin: 14px 0 0;
+ padding: 0;
+}
+
+.EverythingInspector li {
+ border-top: 1px solid #243752;
+ display: grid;
+ gap: 2px;
+ padding: 8px 0;
+}
+
+.EverythingInspector li span {
+ color: #8ca2c0;
+ font-size: 11px;
+ overflow-wrap: anywhere;
+}
+
+.EverythingSiteDetail {
+ display: grid;
+ gap: 8px;
+ margin-top: 14px;
+}
+
+.EverythingSiteDetail span {
+ color: #8ca2c0;
+ font-size: 11px;
+ overflow-wrap: anywhere;
+}
+
+.EverythingTimeline {
+ min-height: 112px;
+}
+
+.EverythingTrack {
+ display: grid;
+ flex: 1;
+ grid-template-rows: 52px 18px 16px;
+ min-width: 0;
+}
+
+.EverythingDensity {
+ height: 52px;
+ opacity: 0.82;
+ width: 100%;
+}
+
+.EverythingTrack input {
+ margin: -8px 0 0;
+ position: relative;
+ width: 100%;
+ z-index: 2;
+}
+
+.EverythingScale {
+ color: #7891b2;
+ display: flex;
+ font-size: 10px;
+ justify-content: space-between;
+}
+
.PlayButton {
display: grid;
width: 38px;
diff --git a/packages/visualizations/src/trackedEverythingModel.ts b/packages/visualizations/src/trackedEverythingModel.ts
new file mode 100644
index 0000000000..b71573fa26
--- /dev/null
+++ b/packages/visualizations/src/trackedEverythingModel.ts
@@ -0,0 +1,139 @@
+import type { TrackedEverythingDataset, TrackedEverythingTimeMark } from './trackedEverythingTypes.ts'
+
+export const isTrackedEverythingDataset = (value: unknown): value is TrackedEverythingDataset => {
+ if (!value || typeof value !== 'object') {
+ return false
+ }
+ const candidate = value as Partial
+ return (
+ candidate.kind === 'tracked-everything' &&
+ candidate.schemaVersion === 1 &&
+ typeof candidate.scenario === 'string' &&
+ typeof candidate.eventFile === 'string' &&
+ candidate.eventFile.length > 0 &&
+ !/^(?:[a-z]+:|[\\/])/i.test(candidate.eventFile) &&
+ Number.isInteger(candidate.eventCount) &&
+ candidate.eventCount! >= 0 &&
+ Number.isFinite(candidate.durationMs) &&
+ candidate.durationMs! >= 0 &&
+ Array.isArray(candidate.sites) &&
+ candidate.sites.every(
+ (site, index) =>
+ Number.isInteger(site?.id) &&
+ site.id === index &&
+ typeof site?.location === 'string' &&
+ typeof site?.type === 'string' &&
+ (site.originalSource === null || typeof site.originalSource === 'string') &&
+ (site.originalLocation === null || typeof site.originalLocation === 'string') &&
+ (site.originalLine === null || Number.isInteger(site.originalLine)) &&
+ (site.originalColumn === null || Number.isInteger(site.originalColumn)),
+ ) &&
+ Array.isArray(candidate.timeMarks) &&
+ candidate.timeMarks.length > 0 &&
+ candidate.timeMarks.every(
+ (mark, index) =>
+ Number.isInteger(mark?.eventIndex) &&
+ mark.eventIndex >= 0 &&
+ mark.eventIndex <= candidate.eventCount! &&
+ Number.isFinite(mark?.elapsedMs) &&
+ mark.elapsedMs >= 0 &&
+ mark.elapsedMs <= candidate.durationMs! &&
+ (index === 0 ||
+ (mark.eventIndex >= candidate.timeMarks![index - 1].eventIndex && mark.elapsedMs >= candidate.timeMarks![index - 1].elapsedMs)),
+ ) &&
+ candidate.timeMarks[0].eventIndex === 0 &&
+ candidate.timeMarks.at(-1)!.eventIndex === candidate.eventCount
+ )
+}
+
+export const getSitePath = (site: TrackedEverythingDataset['sites'][number]): string => {
+ if (site.originalSource) {
+ return site.originalSource.replaceAll('\\', '/')
+ }
+ const match = site.location.match(/^(.+):\d+:\d+$/)
+ return (match?.[1] || 'runtime/unmapped').replaceAll('\\', '/')
+}
+
+export const decodeTrackedEverythingEvents = (buffer: ArrayBuffer, dataset: TrackedEverythingDataset): Uint32Array => {
+ if (buffer.byteLength !== dataset.eventCount * 4) {
+ throw new Error(`Invalid event stream length: expected ${dataset.eventCount * 4} bytes, received ${buffer.byteLength}`)
+ }
+ const endianProbe = new Uint16Array([1])
+ const isLittleEndian = new Uint8Array(endianProbe.buffer)[0] === 1
+ const events = isLittleEndian
+ ? new Uint32Array(buffer)
+ : Uint32Array.from({ length: dataset.eventCount }, (_, index) => new DataView(buffer).getUint32(index * 4, true))
+ for (const siteId of events) {
+ if (!dataset.sites[siteId] || dataset.sites[siteId].id !== siteId) {
+ throw new Error(`Invalid tracked-everything site id ${siteId}`)
+ }
+ }
+ return events
+}
+
+export const moveTrackedEverythingCursor = (
+ events: Uint32Array,
+ counts: Uint32Array,
+ currentCursor: number,
+ requestedCursor: number,
+): number => {
+ const nextCursor = Math.max(0, Math.min(events.length, Math.round(requestedCursor)))
+ if (nextCursor > currentCursor) {
+ for (let index = currentCursor; index < nextCursor; index++) {
+ counts[events[index]]++
+ }
+ } else {
+ for (let index = currentCursor - 1; index >= nextCursor; index--) {
+ counts[events[index]]--
+ }
+ }
+ return nextCursor
+}
+
+export const timeToEventIndex = (timeMarks: readonly TrackedEverythingTimeMark[], elapsedMs: number, eventCount: number): number => {
+ if (timeMarks.length === 0 || elapsedMs <= timeMarks[0].elapsedMs) {
+ return 0
+ }
+ const last = timeMarks.at(-1)!
+ if (elapsedMs >= last.elapsedMs) {
+ return eventCount
+ }
+ let low = 0
+ let high = timeMarks.length - 1
+ while (low + 1 < high) {
+ const middle = (low + high) >> 1
+ if (timeMarks[middle].elapsedMs <= elapsedMs) {
+ low = middle
+ } else {
+ high = middle
+ }
+ }
+ const left = timeMarks[low]
+ const right = timeMarks[high]
+ const progress = right.elapsedMs === left.elapsedMs ? 0 : (elapsedMs - left.elapsedMs) / (right.elapsedMs - left.elapsedMs)
+ return Math.max(0, Math.min(eventCount, Math.round(left.eventIndex + (right.eventIndex - left.eventIndex) * progress)))
+}
+
+export const eventIndexToTime = (timeMarks: readonly TrackedEverythingTimeMark[], eventIndex: number, durationMs: number): number => {
+ if (timeMarks.length === 0 || eventIndex <= 0) {
+ return 0
+ }
+ const last = timeMarks.at(-1)!
+ if (eventIndex >= last.eventIndex) {
+ return durationMs
+ }
+ let low = 0
+ let high = timeMarks.length - 1
+ while (low + 1 < high) {
+ const middle = (low + high) >> 1
+ if (timeMarks[middle].eventIndex <= eventIndex) {
+ low = middle
+ } else {
+ high = middle
+ }
+ }
+ const left = timeMarks[low]
+ const right = timeMarks[high]
+ const progress = right.eventIndex === left.eventIndex ? 0 : (eventIndex - left.eventIndex) / (right.eventIndex - left.eventIndex)
+ return left.elapsedMs + (right.elapsedMs - left.elapsedMs) * progress
+}
diff --git a/packages/visualizations/src/trackedEverythingTypes.ts b/packages/visualizations/src/trackedEverythingTypes.ts
new file mode 100644
index 0000000000..87d0734af6
--- /dev/null
+++ b/packages/visualizations/src/trackedEverythingTypes.ts
@@ -0,0 +1,36 @@
+export interface TrackedEverythingSite {
+ readonly id: number
+ readonly location: string
+ readonly originalColumn: number | null
+ readonly originalLine: number | null
+ readonly originalLocation: string | null
+ readonly originalSource: string | null
+ readonly type: string
+}
+
+export interface TrackedEverythingTimeMark {
+ readonly elapsedMs: number
+ readonly eventIndex: number
+}
+
+export interface TrackedEverythingDataset {
+ readonly durationMs: number
+ readonly eventCount: number
+ readonly eventFile: string
+ readonly kind: 'tracked-everything'
+ readonly scenario: string
+ readonly schemaVersion: 1
+ readonly sites: readonly TrackedEverythingSite[]
+ readonly timeMarks: readonly TrackedEverythingTimeMark[]
+}
+
+export interface TrackedEverythingAggregates {
+ readonly cursor: number
+ readonly fileCounts: Readonly>
+ readonly recentFileCounts: Readonly>
+ readonly recentSiteCounts: readonly number[]
+ readonly siteCounts: readonly number[]
+ readonly timeline: Readonly>
+ readonly typeCounts: Readonly>
+ readonly types: readonly string[]
+}
diff --git a/packages/visualizations/src/trackedEverythingWorker.ts b/packages/visualizations/src/trackedEverythingWorker.ts
new file mode 100644
index 0000000000..6b5879f2e1
--- /dev/null
+++ b/packages/visualizations/src/trackedEverythingWorker.ts
@@ -0,0 +1,81 @@
+///
+
+import { getSitePath, moveTrackedEverythingCursor } from './trackedEverythingModel.ts'
+import type { TrackedEverythingAggregates, TrackedEverythingDataset } from './trackedEverythingTypes.ts'
+
+let dataset: TrackedEverythingDataset
+let events = new Uint32Array()
+let counts = new Uint32Array()
+let cursor = 0
+let selectedType = ''
+const timelineBinCount = 320
+let timeline: Record = Object.create(null)
+let types: string[] = []
+
+const buildTimeline = (): void => {
+ types = [...new Set(dataset.sites.map((site) => site.type))].sort()
+ timeline = Object.fromEntries(types.map((type) => [type, Array.from({ length: timelineBinCount }, () => 0)]))
+ for (let index = 0; index < events.length; index++) {
+ const site = dataset.sites[events[index]]
+ const bin = Math.min(timelineBinCount - 1, Math.floor((index / Math.max(1, events.length)) * timelineBinCount))
+ timeline[site.type][bin]++
+ }
+}
+
+const moveCursor = (nextCursor: number): void => {
+ cursor = moveTrackedEverythingCursor(events, counts, cursor, nextCursor)
+}
+
+const publish = (): void => {
+ const fileCounts: Record = Object.create(null)
+ const recentFileCounts: Record = Object.create(null)
+ const recentSiteCounts = new Uint32Array(counts.length)
+ const typeCounts: Record = Object.create(null)
+ for (let index = Math.max(0, cursor - 1024); index < cursor; index++) {
+ recentSiteCounts[events[index]]++
+ }
+ for (let siteId = 0; siteId < counts.length; siteId++) {
+ const count = counts[siteId]
+ if (count === 0) {
+ continue
+ }
+ const site = dataset.sites[siteId]
+ typeCounts[site.type] = (typeCounts[site.type] || 0) + count
+ if (!selectedType || site.type === selectedType) {
+ const path = getSitePath(site)
+ fileCounts[path] = (fileCounts[path] || 0) + count
+ recentFileCounts[path] = (recentFileCounts[path] || 0) + recentSiteCounts[siteId]
+ }
+ }
+ const message: TrackedEverythingAggregates & { readonly kind: 'aggregates' } = {
+ cursor,
+ fileCounts,
+ kind: 'aggregates',
+ recentFileCounts,
+ recentSiteCounts: Array.from(recentSiteCounts),
+ siteCounts: Array.from(counts),
+ timeline,
+ typeCounts,
+ types,
+ }
+ postMessage(message)
+}
+
+self.onmessage = (
+ event: MessageEvent<
+ | { readonly buffer: ArrayBuffer; readonly dataset: TrackedEverythingDataset; readonly kind: 'init' }
+ | { readonly cursor: number; readonly kind: 'cursor'; readonly selectedType: string }
+ >,
+): void => {
+ if (event.data.kind === 'init') {
+ dataset = event.data.dataset
+ events = new Uint32Array(event.data.buffer)
+ counts = new Uint32Array(dataset.sites.length)
+ buildTimeline()
+ publish()
+ return
+ }
+ selectedType = event.data.selectedType
+ moveCursor(event.data.cursor)
+ publish()
+}
diff --git a/packages/visualizations/test/GenerateTrackedEverything.test.ts b/packages/visualizations/test/GenerateTrackedEverything.test.ts
new file mode 100644
index 0000000000..03532fd217
--- /dev/null
+++ b/packages/visualizations/test/GenerateTrackedEverything.test.ts
@@ -0,0 +1,36 @@
+import { expect, test } from '@jest/globals'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+import { generateTrackedEverything } from '../src/generateTrackedEverything.ts'
+
+test('generates a standalone tracked-everything viewer beside its event stream', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'tracked-everything-viewer-'))
+ const results = join(root, 'results')
+ const assets = join(root, 'assets')
+ const out = join(root, 'out')
+ await mkdir(join(results, 'nested'), { recursive: true })
+ await mkdir(assets, { recursive: true })
+ await writeFile(join(assets, 'index.html'), 'VS Code Memory City')
+ await writeFile(join(results, 'nested', 'scenario.events.bin'), Buffer.alloc(4))
+ await writeFile(
+ join(results, 'nested', 'scenario.json'),
+ JSON.stringify({
+ durationMs: 1,
+ eventCount: 1,
+ eventFile: 'scenario.events.bin',
+ schemaVersion: 1,
+ sites: [{ id: 0, location: 'x:1:2', type: 'Object' }],
+ timeMarks: [{ elapsedMs: 0, eventIndex: 0 }],
+ }),
+ )
+ const outputs = await generateTrackedEverything(results, assets, out)
+ expect(outputs).toEqual([join(out, 'nested', 'scenario')])
+ expect(await readFile(join(outputs[0], 'scenario.events.bin'))).toEqual(Buffer.alloc(4))
+ expect(await readFile(join(outputs[0], 'index.html'), 'utf8')).toContain('tracked-everything-data.js')
+ expect(JSON.parse(await readFile(join(outputs[0], 'tracked-everything.json'), 'utf8'))).toEqual(
+ expect.objectContaining({ kind: 'tracked-everything', scenario: 'nested/scenario' }),
+ )
+ expect(await readFile(join(out, 'index.html'), 'utf8')).toContain('nested/scenario/index.html')
+ await rm(root, { force: true, recursive: true })
+})
diff --git a/packages/visualizations/test/TrackedEverythingModel.test.ts b/packages/visualizations/test/TrackedEverythingModel.test.ts
new file mode 100644
index 0000000000..50cf2f496e
--- /dev/null
+++ b/packages/visualizations/test/TrackedEverythingModel.test.ts
@@ -0,0 +1,95 @@
+import { expect, test } from '@jest/globals'
+import {
+ decodeTrackedEverythingEvents,
+ eventIndexToTime,
+ getSitePath,
+ isTrackedEverythingDataset,
+ moveTrackedEverythingCursor,
+ timeToEventIndex,
+} from '../src/trackedEverythingModel.ts'
+import type { TrackedEverythingDataset } from '../src/trackedEverythingTypes.ts'
+
+const dataset: TrackedEverythingDataset = {
+ durationMs: 100,
+ eventCount: 4,
+ eventFile: 'events.bin',
+ kind: 'tracked-everything',
+ scenario: 'startup',
+ schemaVersion: 1,
+ sites: [
+ {
+ id: 0,
+ location: '/out/workbench.js:1:2',
+ originalColumn: 4,
+ originalLine: 3,
+ originalLocation: 'src/workbench.ts:3:4',
+ originalSource: 'src/workbench.ts',
+ type: 'Object',
+ },
+ {
+ id: 1,
+ location: '/out/editor.js:5:6',
+ originalColumn: null,
+ originalLine: null,
+ originalLocation: null,
+ originalSource: null,
+ type: 'String',
+ },
+ ],
+ timeMarks: [
+ { elapsedMs: 0, eventIndex: 0 },
+ { elapsedMs: 40, eventIndex: 2 },
+ { elapsedMs: 100, eventIndex: 4 },
+ ],
+}
+
+test('validates the tracked-everything dataset contract', () => {
+ expect(isTrackedEverythingDataset(dataset)).toBe(true)
+ expect(isTrackedEverythingDataset({ ...dataset, kind: 'other' })).toBe(false)
+ expect(isTrackedEverythingDataset({ ...dataset, eventCount: 1.5 })).toBe(false)
+ expect(isTrackedEverythingDataset({ ...dataset, eventFile: '/absolute/events.bin' })).toBe(false)
+ expect(isTrackedEverythingDataset({ ...dataset, sites: [{ ...dataset.sites[0], id: 1 }, dataset.sites[1]] })).toBe(false)
+ expect(isTrackedEverythingDataset({ ...dataset, timeMarks: dataset.timeMarks.slice(0, -1) })).toBe(false)
+})
+
+test('validates and decodes the little event stream', () => {
+ const events = new Uint32Array([0, 1, 1, 0])
+ expect([...decodeTrackedEverythingEvents(events.buffer, dataset)]).toEqual([0, 1, 1, 0])
+ expect(() => decodeTrackedEverythingEvents(new Uint32Array([0, 2, 1, 0]).buffer, dataset)).toThrow('Invalid tracked-everything site id 2')
+ expect(() => decodeTrackedEverythingEvents(new Uint32Array([0]).buffer, dataset)).toThrow('Invalid event stream length')
+})
+
+test('maps between sparse elapsed-time checkpoints and exact allocation indexes', () => {
+ expect(timeToEventIndex(dataset.timeMarks, 0, 4)).toBe(0)
+ expect(timeToEventIndex(dataset.timeMarks, 20, 4)).toBe(1)
+ expect(timeToEventIndex(dataset.timeMarks, 70, 4)).toBe(3)
+ expect(timeToEventIndex(dataset.timeMarks, 100, 4)).toBe(4)
+ expect(eventIndexToTime(dataset.timeMarks, 1, 100)).toBe(20)
+ expect(eventIndexToTime(dataset.timeMarks, 3, 100)).toBe(70)
+})
+
+test('prefers original source paths and falls back to generated paths', () => {
+ expect(getSitePath(dataset.sites[0])).toBe('src/workbench.ts')
+ expect(getSitePath(dataset.sites[1])).toBe('/out/editor.js')
+})
+
+test('decodes a synthetic 400,000-event capture without losing order', () => {
+ const largeDataset = { ...dataset, eventCount: 400_000 }
+ const source = new Uint32Array(largeDataset.eventCount)
+ for (let index = 0; index < source.length; index++) {
+ source[index] = index % 2
+ }
+ const decoded = decodeTrackedEverythingEvents(source.buffer, largeDataset)
+ expect(decoded).toHaveLength(400_000)
+ expect(decoded[0]).toBe(0)
+ expect(decoded[399_999]).toBe(1)
+
+ const counts = new Uint32Array(2)
+ let cursor = moveTrackedEverythingCursor(decoded, counts, 0, decoded.length)
+ expect([...counts]).toEqual([200_000, 200_000])
+ cursor = moveTrackedEverythingCursor(decoded, counts, cursor, 123_456)
+ expect([...counts]).toEqual([61_728, 61_728])
+ cursor = moveTrackedEverythingCursor(decoded, counts, cursor, 0)
+ expect(cursor).toBe(0)
+ expect([...counts]).toEqual([0, 0])
+})