Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
225 changes: 225 additions & 0 deletions src/bscPlugin/transpile/BrsFilePreTranspileProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -45,4 +46,228 @@ describe('BrsFile', () => {
await program.transpile([], s`${tempDir}/out`);
});
});

describe('perfettoTracing', () => {
let tracingProgram: Program;
let testTranspile: ReturnType<typeof getTestTranspile>;

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
`);
});
});
});
77 changes: 73 additions & 4 deletions src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<ClassStatement>(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<FunctionExpression>(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<FunctionExpression>(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
Expand Down
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
3 changes: 2 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down