Skip to content

Commit 11e3e05

Browse files
authored
Merge pull request #1053 from OpenFn/ignore-top-object-add-import
feat: Compiler optimisations
2 parents 49caf75 + 3d47c8b commit 11e3e05

File tree

11 files changed

+127
-59
lines changed

11 files changed

+127
-59
lines changed

.changeset/floppy-comics-work.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@openfn/compiler': patch
3+
'@openfn/ws-worker': patch
4+
---
5+
6+
Improve compiler performance for large files
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { namedTypes as n } from 'ast-types';
2+
import { NodePath } from 'ast-types/lib/node-path';
3+
4+
const IgnoreRules = (path: NodePath<n.Expression>) => {
5+
return {
6+
check: n.ObjectExpression.check(path.node),
7+
shouldSkip: () => {
8+
if (
9+
n.VariableDeclarator.check(path.parentPath.node) &&
10+
n.VariableDeclaration.check(path.parentPath.parentPath.node) &&
11+
n.Program.check(path.parentPath.parentPath.parentPath.parentPath.node)
12+
) {
13+
return true;
14+
}
15+
return false;
16+
},
17+
};
18+
};
19+
20+
export default IgnoreRules;

packages/compiler/src/transform.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export default function transform(
6868
] as Transformer[];
6969
}
7070
const logger = options.logger || defaultLogger;
71-
7271
transformers
7372
// Ignore transformers which are explicitly disabled
7473
.filter(({ id }) => options[id] ?? true)
@@ -82,11 +81,13 @@ export default function transform(
8281
})
8382
// Run each transformer
8483
.forEach(({ id, types, visitor }) => {
84+
const t = `transformer ${id}`;
85+
console.time?.(t);
8586
const astTypes: Visitor = {};
8687
for (const type of types) {
8788
const name = `visit${type}` as keyof Visitor;
8889
astTypes[name] = function (path: NodePath) {
89-
printHeap(`visit: ${path.name}`);
90+
// printHeap(`visit: ${path.name}`);
9091
const opts = options[id] || {};
9192
const abort = visitor!(path, logger, opts);
9293
if (abort) {
@@ -98,8 +99,15 @@ export default function transform(
9899

99100
// @ts-ignore
100101
visit(ast, astTypes);
102+
if (options.trace) {
103+
console.log();
104+
printHeap(`${t}`);
105+
console.timeEnd?.(t);
106+
}
101107
});
102-
108+
if (options.trace) {
109+
console.log();
110+
}
103111
printHeap(`finished`);
104112
const duration = (Date.now() - start) / 1000;
105113
logger.debug(`Finished in ${duration}s`);

packages/compiler/src/transforms/add-imports.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ export type IdentifierList = Record<string, true>;
107107
export function findAllDanglingIdentifiers(ast: ASTNode) {
108108
const result: IdentifierList = {};
109109
visit(ast, {
110+
visitObjectExpression(path) {
111+
// excaping top-level object variable declarations.
112+
if (
113+
n.VariableDeclarator.check(path.parentPath.node) &&
114+
n.VariableDeclaration.check(path.parentPath.parentPath.node) &&
115+
n.Program.check(path.parentPath.parentPath.parentPath.parentPath.node)
116+
) {
117+
return false;
118+
}
119+
this.traverse(path);
120+
},
110121
visitIdentifier: function (path) {
111122
// If this is part of an import statement, do nothing
112123
if (n.ImportSpecifier.check(path.parent.node)) {

packages/compiler/src/transforms/lazy-state.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { builders as b, namedTypes as n } from 'ast-types';
1010
import type { NodePath } from 'ast-types/lib/node-path';
1111
import type { Transformer } from '../transform';
12+
import IgnoreRules from '../transform-ignore';
1213

1314
// Walk up the AST and work out where the parent arrow function should go
1415
const ensureParentArrow = (path: NodePath<n.MemberExpression>) => {
@@ -79,6 +80,11 @@ const isOpenFunction = (path: NodePath) => {
7980
};
8081

8182
function visitor(path: NodePath<n.MemberExpression>) {
83+
// if it was called for an ObjectExpression
84+
const ignoreRule = IgnoreRules(path);
85+
if (ignoreRule.check) {
86+
return ignoreRule.shouldSkip();
87+
}
8288
let first = path.node.object;
8389
while (first.hasOwnProperty('object')) {
8490
first = (first as n.MemberExpression).object;
@@ -106,7 +112,7 @@ function visitor(path: NodePath<n.MemberExpression>) {
106112

107113
export default {
108114
id: 'lazy-state',
109-
types: ['MemberExpression'],
115+
types: ['MemberExpression', 'ObjectExpression'],
110116
visitor,
111117
// It's important that $ symbols are escaped before any other transformations can run
112118
order: 0,

packages/compiler/src/transforms/promises.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { namedTypes as n, builders as b } from 'ast-types';
22

33
import type { NodePath } from 'ast-types/lib/node-path';
4+
import IgnoreRules from '../transform-ignore';
45

56
const NO_DEFER_DECLARATION_ERROR = 'No defer declaration found';
67

@@ -146,6 +147,10 @@ const isTopScope = (path: NodePath<any>) => {
146147
};
147148

148149
const visitor = (path: NodePath<n.CallExpression>) => {
150+
const ignoreRule = IgnoreRules(path);
151+
if (ignoreRule.check) {
152+
return ignoreRule.shouldSkip();
153+
}
149154
let root: NodePath<any> = path;
150155
while (!n.Program.check(root.node)) {
151156
root = root.parent;
@@ -166,7 +171,7 @@ const visitor = (path: NodePath<n.CallExpression>) => {
166171

167172
export default {
168173
id: 'promises',
169-
types: ['CallExpression'],
174+
types: ['CallExpression', 'ObjectExpression'],
170175
visitor,
171176
// this should run before top-level operations are moved into the exports array
172177
order: 0,

packages/compiler/src/transforms/top-level-operations.ts

Lines changed: 32 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,71 +8,53 @@ import type { Transformer } from '../transform';
88
// Note that the validator should complain if it see anything other than export default []
99
// What is the relationship between the validator and the compiler?
1010

11-
export type ExtendedProgram = NodePath<namedTypes.Program> & {
12-
operations: Array<{ line: number; name: string; order: number }>;
13-
};
11+
export type ExtendedProgram = NodePath<
12+
namedTypes.Program & {
13+
operations: Array<{ line: number; name: string; order: number }>;
14+
}
15+
>;
1416

1517
export type TopLevelOpsOptions = {
1618
// Wrap operations in a `(state) => op` wrapper
1719
wrap: boolean; // TODO
1820
};
1921

20-
function visitor(path: NodePath<namedTypes.CallExpression>) {
21-
const root = path.parent.parent.node;
22-
// Check if the node is a top level Operation
22+
function visitor(programPath: ExtendedProgram) {
23+
const operations: Array<{ line: number; name: string; order: number }> = [];
24+
const children = programPath.node.body;
25+
const rem = [];
26+
27+
const target = programPath.node.body.at(-1);
2328
if (
24-
// Check this is a top level call expression
25-
// ie, the parent must be an ExpressionStatement, and the statement's parent must be a Program
26-
n.Program.check(root) &&
27-
n.Statement.check(path.parent.node) &&
28-
// An Operation could be an Identifier (fn()) or Member Expression (http.get())
29-
(n.Identifier.check(path.node.callee) ||
30-
n.MemberExpression.check(path.node.callee))
29+
n.ExportDefaultDeclaration.check(target) &&
30+
n.ArrayExpression.check(target.declaration)
3131
) {
32-
appendOperationMetadata(path, root as unknown as ExtendedProgram);
33-
34-
// Now Find the top level exports array
35-
const target = root.body.at(-1);
36-
if (
37-
n.ExportDefaultDeclaration.check(target) &&
38-
n.ArrayExpression.check(target.declaration)
39-
) {
40-
const statement = path.parent;
41-
target.declaration.elements.push(path.node);
42-
// orphan the original expression statement parent
43-
statement.prune();
44-
} else {
45-
// error! there isn't an appropriate export statement
46-
// What do we do?
32+
for (const child of children) {
33+
if (
34+
n.ExpressionStatement.check(child) &&
35+
n.CallExpression.check(child.expression)
36+
) {
37+
const order = operations.length + 1;
38+
// @ts-ignore
39+
const name = child.expression.callee.name;
40+
const line = child.expression.loc?.start.line ?? -1;
41+
operations.push({ name, line, order });
42+
target.declaration.elements.push(child.expression as any);
43+
} else rem.push(child);
4744
}
45+
programPath.node.body = rem;
46+
} else {
47+
// error! there isn't an appropriate export statement
48+
// What do we do?
4849
}
50+
programPath.node.operations = operations;
4951

5052
// if not (for now) we should cancel traversal
5153
return true;
5254
}
5355

54-
// Add metadata to each operation for:
55-
// - the order (do we know? We can guess from the state of the exports array
56-
// - the name of the operation
57-
// - the original line number (do we know?)
58-
function appendOperationMetadata(
59-
path: NodePath<namedTypes.CallExpression>,
60-
root: ExtendedProgram
61-
) {
62-
if (!root.operations) {
63-
root.operations = [];
64-
}
65-
66-
const { operations } = root;
67-
const order = operations.length + 1;
68-
const name = path.value.callee.name;
69-
const line = path.value.loc?.start.line ?? -1;
70-
71-
operations.push({ line, order, name });
72-
}
73-
7456
export default {
7557
id: 'top-level-operations',
76-
types: ['CallExpression'],
58+
types: ['Program'],
7759
visitor,
78-
} as Transformer;
60+
} as unknown as Transformer;

packages/compiler/src/util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import path from 'node:path';
55
import { Project, describeDts } from '@openfn/describe-package';
66
import type { Logger } from '@openfn/logger';
77

8-
export function heap(reason: string, logger?: Logger) {
8+
export function heap(reason: string, _logger?: Logger) {
99
const { used_heap_size } = getHeapStatistics();
1010
const mb = used_heap_size / 1024 / 1024;
11-
logger?.debug(`[${reason}] Used heap at ${mb.toFixed(2)}mb`);
11+
console.log(`[${reason}] Used heap at ${mb.toFixed(2)}mb`);
1212
}
1313

1414
export const loadFile = (filePath: string) =>

packages/compiler/test/transforms/add-imports.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,23 @@ test('findAllDanglingIdentifiers: const x = { a: 10 };', (t) => {
147147
t.assert(Object.keys(result).length == 0);
148148
});
149149

150-
test('findAllDanglingIdentifiers: const x = { a: b };', (t) => {
150+
test('findAllDanglingIdentifiers: ignore top-level objects declarations', (t) => {
151+
// ignored because [bank] isn't picked as dangling but [foo] is
152+
const ast = parse(`
153+
const bigObj = {name: "ignore-me", age: bank};
154+
fn(state=> {
155+
const update = {
156+
name: "pick-me",
157+
age: foo
158+
}
159+
return update
160+
})
161+
`);
162+
const result = findAllDanglingIdentifiers(ast);
163+
t.deepEqual(result, { fn: true, foo: true });
164+
});
165+
166+
test.skip('findAllDanglingIdentifiers: const x = { a: b };', (t) => {
151167
const ast = parse('const x = { a: b };');
152168
const result = findAllDanglingIdentifiers(ast);
153169
t.assert(Object.keys(result).length == 1);

packages/compiler/test/transforms/lazy-state.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ test('convert a simple dollar reference', (t) => {
1515
t.is(code, 'get(state => state.data)');
1616
});
1717

18+
test("don't visit top-level object", (t) => {
19+
const ast = parse(`const x = { x: $.data };
20+
fn($.data)`);
21+
t.notThrows(() => {
22+
const transformed = transform(ast, [visitors]);
23+
const { code } = print(transformed);
24+
t.is(
25+
code,
26+
`const x = { x: $.data };
27+
fn(state => state.data)`
28+
);
29+
});
30+
});
31+
1832
test('convert a chained dollar reference', (t) => {
1933
const ast = parse('get($.a.b.c.d)');
2034

0 commit comments

Comments
 (0)