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..0afcc51c8 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,228 @@ 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 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() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0") + print "I am anon" + end function + callback() + end function + `); + }); + + it('does 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() + 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() + b() + end function + `); + }); + + it('does 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() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0") + b = function() + bsc__trace = CreateObject("roPerfetto").createScopedEvent("outer$anon0$anon0") + 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