From 58cde64a2e051586aed84517a4db692641eed505 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Tue, 24 Mar 2026 08:40:58 -0300 Subject: [PATCH 1/2] first draft of add support for injecting function level perfetto tracing --- src/BsConfig.ts | 8 + .../BrsFilePreTranspileProcessor.spec.ts | 220 ++++++++++++++++++ .../transpile/BrsFilePreTranspileProcessor.ts | 77 +++++- src/cli.ts | 1 + src/util.ts | 3 +- 5 files changed, 304 insertions(+), 5 deletions(-) diff --git a/src/BsConfig.ts b/src/BsConfig.ts index 05cb40271..51ac3909f 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -213,6 +213,14 @@ export interface BsConfig { * scripts inside `source` that depend on bslib.brs. Defaults to `source`. */ bslibDestinationDir?: string; + + /** + * When enabled, injects a Perfetto scoped tracing statement at the top of every + * transpiled function and method body: + * `bsc__trace = CreateObject("roPerfetto").createScopedEvent("function_name")` + * @default false + */ + perfettoTracing?: boolean; } type OptionalBsConfigFields = diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts index b74ab300c..78b79f2dd 100644 --- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts +++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts @@ -3,6 +3,7 @@ import * as fsExtra from 'fs-extra'; import { Program } from '../../Program'; import { standardizePath as s } from '../../util'; import { tempDir, rootDir } from '../../testHelpers.spec'; +import { getTestTranspile } from '../../testHelpers.spec'; import { LogLevel, createLogger } from '../../logging'; import PluginInterface from '../../PluginInterface'; const sinon = createSandbox(); @@ -45,4 +46,223 @@ describe('BrsFile', () => { await program.transpile([], s`${tempDir}/out`); }); }); + + describe('perfettoTracing', () => { + let tracingProgram: Program; + let testTranspile: ReturnType; + + beforeEach(() => { + const logger = createLogger({ logLevel: LogLevel.warn }); + tracingProgram = new Program({ rootDir: rootDir, sourceMap: true, perfettoTracing: true }, logger, new PluginInterface([], { + logger: logger, + suppressErrors: false + })); + testTranspile = getTestTranspile(() => [tracingProgram, rootDir]); + }); + + afterEach(() => { + tracingProgram.dispose(); + }); + + it('does not inject trace statements when perfettoTracing is disabled', () => { + // The outer `program` has no perfettoTracing option + const transpile = getTestTranspile(() => [program, rootDir]); + transpile(` + function doSomething() + print "hello" + end function + `, ` + function doSomething() + print "hello" + end function + `); + }); + + it('injects trace into a simple function', () => { + testTranspile(` + function doSomething() + print "hello" + end function + `, ` + function doSomething() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("doSomething") + print "hello" + end function + `); + }); + + it('injects trace into a sub', () => { + testTranspile(` + sub doSomething() + print "hello" + end sub + `, ` + sub doSomething() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("doSomething") + print "hello" + end sub + `); + }); + + it('injects trace into a namespace-prefixed function', () => { + testTranspile(` + namespace MyApp.Utils + function helperFunc() + print "hello" + end function + end namespace + `, ` + function MyApp_Utils_helperFunc() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("MyApp_Utils_helperFunc") + print "hello" + end function + `); + }); + + it('injects trace into a class method', () => { + testTranspile(` + class Animal + function speak() + print "hello" + end function + end class + `, ` + sub __Animal_method_new() + end sub + function __Animal_method_speak() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("__Animal_method_speak") + print "hello" + end function + function __Animal_builder() + instance = {} + instance.new = __Animal_method_new + instance.speak = __Animal_method_speak + return instance + end function + function Animal() + instance = __Animal_builder() + instance.new() + return instance + end function + `, undefined, 'source/main.bs'); + }); + + it('injects trace into a namespaced class method', () => { + testTranspile(` + namespace Birds + class Duck + function quack() + print "quack" + end function + end class + end namespace + `, ` + sub __Birds_Duck_method_new() + end sub + function __Birds_Duck_method_quack() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("__Birds_Duck_method_quack") + print "quack" + end function + function __Birds_Duck_builder() + instance = {} + instance.new = __Birds_Duck_method_new + instance.quack = __Birds_Duck_method_quack + return instance + end function + function Birds_Duck() + instance = __Birds_Duck_builder() + instance.new() + return instance + end function + `, undefined, 'source/main.bs'); + }); + + it('injects trace into every function in a file', () => { + testTranspile(` + function alpha() + end function + function beta() + end function + `, ` + function alpha() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("alpha") + end function + + function beta() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("beta") + end function + `); + }); + + it('does not inject trace into an anonymous function expression inside a named function', () => { + testTranspile(` + function outer() + callback = function() + print "I am anon" + end function + callback() + end function + `, ` + function outer() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") + callback = function() + print "I am anon" + end function + callback() + end function + `); + }); + + it('does not inject trace into multiple anonymous function expressions inside a named function', () => { + testTranspile(` + function outer() + a = function() + print "anon a" + end function + b = function() + print "anon b" + end function + a() + b() + end function + `, ` + function outer() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") + a = function() + print "anon a" + end function + b = function() + print "anon b" + end function + a() + b() + end function + `); + }); + + it('does not inject trace into a deeply nested anonymous function expression', () => { + testTranspile(` + function outer() + a = function() + b = function() + print "deeply anon" + end function + b() + end function + a() + end function + `, ` + function outer() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") + a = function() + b = function() + print "deeply anon" + end function + b() + end function + a() + end function + `); + }); + }); }); diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts index 7b9c6f269..af776bce1 100644 --- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts @@ -1,15 +1,16 @@ -import { createAssignmentStatement, createBlock, createDottedSetStatement, createIfStatement, createIndexedSetStatement, createToken } from '../../astUtils/creators'; -import { isAssignmentStatement, isBinaryExpression, isBlock, isBody, isBrsFile, isDottedGetExpression, isDottedSetStatement, isGroupingExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isUnaryExpression, isVariableExpression } from '../../astUtils/reflection'; +import { createAssignmentStatement, createBlock, createCall, createDottedSetStatement, createIdentifier, createIfStatement, createIndexedSetStatement, createStringLiteral, createToken, createVariableExpression } from '../../astUtils/creators'; +import { isAssignmentStatement, isBinaryExpression, isBlock, isBody, isBrsFile, isClassStatement, isDottedGetExpression, isDottedSetStatement, isFunctionExpression, isGroupingExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isUnaryExpression, isVariableExpression } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BrsFile } from '../../files/BrsFile'; import type { BeforeFileTranspileEvent } from '../../interfaces'; import type { Token } from '../../lexer/Token'; import { TokenKind } from '../../lexer/TokenKind'; import type { Expression, Statement } from '../../parser/AstNode'; -import type { TernaryExpression } from '../../parser/Expression'; +import { DottedGetExpression } from '../../parser/Expression'; +import type { FunctionExpression, TernaryExpression } from '../../parser/Expression'; import { LiteralExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; -import type { IfStatement } from '../../parser/Statement'; +import type { Block, ClassStatement, IfStatement } from '../../parser/Statement'; import type { Scope } from '../../Scope'; import util from '../../util'; @@ -21,10 +22,78 @@ export class BrsFilePreTranspileProcessor { public process() { if (isBrsFile(this.event.file)) { + this.injectPerfettoTracing(); this.iterateExpressions(); } } + private injectPerfettoTracing() { + if (!this.event.program.options.perfettoTracing) { + return; + } + this.event.file.ast.walk(createVisitor({ + FunctionStatement: (statement) => { + this.injectTraceStatement(statement.func.body, statement.getName(ParseMode.BrightScript)); + }, + MethodStatement: (statement) => { + const classStatement = statement.findAncestor(isClassStatement); + const className = classStatement?.getName(ParseMode.BrightScript) ?? 'unknown'; + const traceName = `__${className}_method_${statement.name.text}`; + this.injectTraceStatement(statement.func.body, traceName); + }, + FunctionExpression: (expression) => { + // Only handle anonymous function expressions (named ones are handled by FunctionStatement/MethodStatement above) + if (expression.functionStatement) { + return; + } + const traceName = this.getAnonFunctionName(expression); + this.injectTraceStatement(expression.body, traceName); + } + }), { walkMode: WalkMode.visitAllRecursive }); + } + + /** + * Compute a name for an anonymous FunctionExpression using the same scheme as SOURCE_FUNCTION_NAME: + * outerFunction$anon0, outerFunction$anon1, outerFunction$anon0$anon0, etc. + */ + private getAnonFunctionName(func: FunctionExpression): string { + const nameParts: string[] = []; + let current = func; + let parentFunc = current.findAncestor(isFunctionExpression); + while (parentFunc) { + const siblings: FunctionExpression[] = []; + parentFunc.walk(createVisitor({ + FunctionExpression: (expr) => { + siblings.push(expr); + } + }), { walkMode: WalkMode.visitAllRecursive }); + nameParts.unshift(`anon${siblings.indexOf(current)}`); + current = parentFunc; + parentFunc = current.findAncestor(isFunctionExpression); + } + const rootName = current.functionStatement?.getName(ParseMode.BrightScript) ?? 'unknown'; + nameParts.unshift(rootName); + return nameParts.join('$'); + } + + private injectTraceStatement(body: Block, funcName: string) { + const traceStatement = createAssignmentStatement({ + name: 'bsc__trace', + value: createCall( + new DottedGetExpression( + createCall( + createVariableExpression('CreateObject'), + [createStringLiteral('roPerfetto')] + ), + createIdentifier('createScopedEvent'), + createToken(TokenKind.Dot) + ), + [createStringLiteral(funcName)] + ) + }); + this.event.editor.arrayUnshift(body.statements, traceStatement); + } + private iterateExpressions() { const scope = this.event.program.getFirstScopeForFile(this.event.file); //TODO move away from this loop and use a visitor instead diff --git a/src/cli.ts b/src/cli.ts index 378d9983c..044809c41 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -42,6 +42,7 @@ let options = yargs .option('source-root', { type: 'string', description: 'Override the root directory path where debugger should locate the source files. The location will be embedded in the source map to help debuggers locate the original source files. This only applies to files found within rootDir. This is useful when you want to preprocess files before passing them to BrighterScript, and want a debugger to open the original files.' }) .option('watch', { type: 'boolean', defaultDescription: 'false', description: 'Watch input files.' }) .option('require', { type: 'array', description: 'A list of modules to require() on startup. Useful for doing things like ts-node registration.' }) + .option('perfetto-tracing', { type: 'boolean', defaultDescription: 'false', description: 'Inject Perfetto scoped tracing statements at the top of every transpiled function body.' }) .option('profile', { type: 'boolean', defaultDescription: 'false', description: 'Generate a cpuprofile report during this run' }) .option('lsp', { type: 'boolean', defaultDescription: 'false', description: 'Run brighterscript as a language server.' }) .check(argv => { diff --git a/src/util.ts b/src/util.ts index db38cfd28..b75c0c21e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -403,7 +403,8 @@ export class Util { emitDefinitions: config.emitDefinitions === true ? true : false, removeParameterTypes: config.removeParameterTypes === true ? true : false, logLevel: logLevel, - bslibDestinationDir: bslibDestinationDir + bslibDestinationDir: bslibDestinationDir, + perfettoTracing: config.perfettoTracing === true ? true : false }; //mutate `config` in case anyone is holding a reference to the incomplete one From 25b572eaecd49e185f939b38878d6f2734a49e18 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Tue, 24 Mar 2026 12:30:11 -0300 Subject: [PATCH 2/2] fixed some tests --- .../transpile/BrsFilePreTranspileProcessor.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts index 78b79f2dd..0afcc51c8 100644 --- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts +++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts @@ -194,7 +194,7 @@ describe('BrsFile', () => { `); }); - it('does not inject trace into an anonymous function expression inside a named function', () => { + it('does inject trace into an anonymous function expression inside a named function', () => { testTranspile(` function outer() callback = function() @@ -206,6 +206,7 @@ describe('BrsFile', () => { function outer() bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") callback = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0") print "I am anon" end function callback() @@ -213,7 +214,7 @@ describe('BrsFile', () => { `); }); - it('does not inject trace into multiple anonymous function expressions inside a named function', () => { + it('does inject trace into multiple anonymous function expressions inside a named function', () => { testTranspile(` function outer() a = function() @@ -229,9 +230,11 @@ describe('BrsFile', () => { function outer() bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") a = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0") print "anon a" end function b = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon1") print "anon b" end function a() @@ -240,7 +243,7 @@ describe('BrsFile', () => { `); }); - it('does not inject trace into a deeply nested anonymous function expression', () => { + it('does inject trace into a deeply nested anonymous function expression', () => { testTranspile(` function outer() a = function() @@ -255,7 +258,9 @@ describe('BrsFile', () => { function outer() bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer") a = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0") b = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0$anon0") print "deeply anon" end function b()