Skip to content

Commit 7451452

Browse files
authored
feat(schema-compiler): Move transpiling to worker threads (under the flag) (#9188)
* move js transpilation to worker threads * hide worker threads transpile under the flag * add useful comment * tune Push CI to test with CUBEJS_WORKER_THREADS_TRANSPILATION=true * add workerpool pkg * use workerpool pkg * add max workers cfg * update env in ci * remove obsolete stuff
1 parent 0dbf3bb commit 7451452

File tree

13 files changed

+264
-356
lines changed

13 files changed

+264
-356
lines changed

.github/workflows/push.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ jobs:
5454
# Current docker version + next LTS
5555
node-version: [20.x, 22.x]
5656
python-version: [3.11]
57+
transpile-worker-threads: [false, true]
5758
fail-fast: false
5859

60+
env:
61+
CUBEJS_TRANSPILATION_WORKER_THREADS: ${{ matrix.transpile-worker-threads }}
5962
steps:
6063
- id: get-tag-out
6164
run: echo "$OUT"

packages/cubejs-backend-shared/src/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ const variables: Record<string, (...args: any) => any> = {
196196
nativeOrchestrator: () => get('CUBEJS_TESSERACT_ORCHESTRATOR')
197197
.default('false')
198198
.asBoolStrict(),
199+
transpilationWorkerThreads: () => get('CUBEJS_TRANSPILATION_WORKER_THREADS')
200+
.default('false')
201+
.asBoolStrict(),
202+
transpilationWorkerThreadsCount: () => get('CUBEJS_TRANSPILATION_WORKER_THREADS_COUNT')
203+
.default('0')
204+
.asInt(),
199205

200206
/** ****************************************************************
201207
* Common db options *

packages/cubejs-schema-compiler/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"node-dijkstra": "^2.5.0",
5555
"ramda": "^0.27.2",
5656
"syntax-error": "^1.3.0",
57-
"uuid": "^8.3.2"
57+
"uuid": "^8.3.2",
58+
"workerpool": "^9.2.0"
5859
},
5960
"devDependencies": {
6061
"@clickhouse/client": "^1.7.0",

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { camelizeCube } from './utils';
88
import { BaseQuery } from '../adapter';
99

1010
const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/;
11-
const CONTEXT_SYMBOLS = {
11+
export const CONTEXT_SYMBOLS = {
1212
SECURITY_CONTEXT: 'securityContext',
1313
// SECURITY_CONTEXT has been deprecated, however security_context (lowecase)
1414
// is allowed in RBAC policies for query-time attribute matching
@@ -19,7 +19,7 @@ const CONTEXT_SYMBOLS = {
1919
SQL_UTILS: 'sqlUtils'
2020
};
2121

22-
const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE'];
22+
export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE'];
2323

2424
export class CubeSymbols {
2525
constructor(evaluateViews) {

packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.js

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { parse } from '@babel/parser';
66
import babelGenerator from '@babel/generator';
77
import babelTraverse from '@babel/traverse';
88
import R from 'ramda';
9+
import workerpool from 'workerpool';
910

10-
import { isNativeSupported } from '@cubejs-backend/shared';
11+
import { getEnv, isNativeSupported } from '@cubejs-backend/shared';
1112
import { UserError } from './UserError';
1213
import { ErrorReporter } from './ErrorReporter';
1314

@@ -28,6 +29,8 @@ export class DataSchemaCompiler {
2829
this.viewCompilationGate = options.viewCompilationGate;
2930
this.cubeNameCompilers = options.cubeNameCompilers || [];
3031
this.extensions = options.extensions || {};
32+
this.cubeDictionary = options.cubeDictionary;
33+
this.cubeSymbols = options.cubeSymbols;
3134
this.cubeFactory = options.cubeFactory;
3235
this.filesToCompile = options.filesToCompile;
3336
this.omitErrors = options.omitErrors;
@@ -40,6 +43,7 @@ export class DataSchemaCompiler {
4043
this.yamlCompiler = options.yamlCompiler;
4144
this.yamlCompiler.dataSchemaCompiler = this;
4245
this.pythonContext = null;
46+
this.workerPool = null;
4347
}
4448

4549
compileObjects(compileServices, objects, errorsReport) {
@@ -89,10 +93,40 @@ export class DataSchemaCompiler {
8993
const errorsReport = new ErrorReporter(null, [], this.errorReport);
9094
this.errorsReport = errorsReport;
9195

92-
// TODO: required in order to get pre transpile compilation work
93-
const transpile = () => toCompile.map(f => this.transpileFile(f, errorsReport)).filter(f => !!f);
96+
if (getEnv('transpilationWorkerThreads')) {
97+
const wc = getEnv('transpilationWorkerThreadsCount');
98+
this.workerPool = workerpool.pool(
99+
path.join(__dirname, 'transpilers/transpiler_worker'),
100+
wc > 0 ? { maxWorkers: wc } : undefined,
101+
);
102+
}
103+
104+
const transpile = async () => {
105+
let cubeNames;
106+
let cubeSymbolsNames;
107+
108+
if (getEnv('transpilationWorkerThreads')) {
109+
cubeNames = Object.keys(this.cubeDictionary.byId);
110+
// We need only cubes and all its member names for transpiling.
111+
// Cubes doesn't change during transpiling, but are changed during compilation phase,
112+
// so we can prepare them once for every phase.
113+
// Communication between main and worker threads uses
114+
// The structured clone algorithm (@see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
115+
// which doesn't allow passing any function objects, so we need to sanitize the symbols.
116+
cubeSymbolsNames = Object.fromEntries(
117+
Object.entries(this.cubeSymbols.symbols)
118+
.map(
119+
([key, value]) => [key, Object.fromEntries(
120+
Object.keys(value).map((k) => [k, true]),
121+
)],
122+
),
123+
);
124+
}
125+
const results = await Promise.all(toCompile.map(f => this.transpileFile(f, errorsReport, { cubeNames, cubeSymbolsNames })));
126+
return results.filter(f => !!f);
127+
};
94128

95-
const compilePhase = (compilers) => this.compileCubeFiles(compilers, transpile(), errorsReport);
129+
const compilePhase = async (compilers) => this.compileCubeFiles(compilers, await transpile(), errorsReport);
96130

97131
return compilePhase({ cubeCompilers: this.cubeNameCompilers })
98132
.then(() => compilePhase({ cubeCompilers: this.preTranspileCubeCompilers.concat([this.viewCompilationGate]) }))
@@ -102,7 +136,12 @@ export class DataSchemaCompiler {
102136
.then(() => compilePhase({
103137
cubeCompilers: this.cubeCompilers,
104138
contextCompilers: this.contextCompilers,
105-
}));
139+
}))
140+
.then(() => {
141+
if (this.workerPool) {
142+
this.workerPool.terminate();
143+
}
144+
});
106145
}
107146

108147
compile() {
@@ -118,7 +157,7 @@ export class DataSchemaCompiler {
118157
return this.compilePromise;
119158
}
120159

121-
transpileFile(file, errorsReport) {
160+
async transpileFile(file, errorsReport, options) {
122161
if (R.endsWith('.jinja', file.fileName) ||
123162
(R.endsWith('.yml', file.fileName) || R.endsWith('.yaml', file.fileName))
124163
// TODO do Jinja syntax check with jinja compiler
@@ -137,31 +176,47 @@ export class DataSchemaCompiler {
137176
} else if (R.endsWith('.yml', file.fileName) || R.endsWith('.yaml', file.fileName)) {
138177
return file;
139178
} else if (R.endsWith('.js', file.fileName)) {
140-
return this.transpileJsFile(file, errorsReport);
179+
return this.transpileJsFile(file, errorsReport, options);
141180
} else {
142181
return file;
143182
}
144183
}
145184

146-
transpileJsFile(file, errorsReport) {
185+
async transpileJsFile(file, errorsReport, { cubeNames, cubeSymbolsNames }) {
147186
try {
148-
const ast = parse(
149-
file.content,
150-
{
151-
sourceFilename: file.fileName,
152-
sourceType: 'module',
153-
plugins: ['objectRestSpread']
154-
},
155-
);
187+
if (getEnv('transpilationWorkerThreads')) {
188+
const data = {
189+
fileName: file.fileName,
190+
content: file.content,
191+
transpilers: this.transpilers.map(t => t.constructor.name),
192+
cubeNames,
193+
cubeSymbolsNames,
194+
};
195+
196+
const res = await this.workerPool.exec('transpile', [data]);
197+
errorsReport.addErrors(res.errors);
198+
errorsReport.addWarnings(res.warnings);
199+
200+
return Object.assign({}, file, { content: res.content });
201+
} else {
202+
const ast = parse(
203+
file.content,
204+
{
205+
sourceFilename: file.fileName,
206+
sourceType: 'module',
207+
plugins: ['objectRestSpread'],
208+
},
209+
);
156210

157-
this.transpilers.forEach((t) => {
158-
errorsReport.inFile(file);
159-
babelTraverse(ast, t.traverseObject(errorsReport));
160-
errorsReport.exitFile();
161-
});
211+
this.transpilers.forEach((t) => {
212+
errorsReport.inFile(file);
213+
babelTraverse(ast, t.traverseObject(errorsReport));
214+
errorsReport.exitFile();
215+
});
162216

163-
const content = babelGenerator(ast, {}, file.content).code;
164-
return Object.assign({}, file, { content });
217+
const content = babelGenerator(ast, {}, file.content).code;
218+
return Object.assign({}, file, { content });
219+
}
165220
} catch (e) {
166221
if (e.toString().indexOf('SyntaxError') !== -1) {
167222
const line = file.content.split('\n')[e.loc.line - 1];

packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ export class ErrorReporter {
137137
}
138138
}
139139

140+
public getErrors() {
141+
return this.rootReporter().errors;
142+
}
143+
144+
public addErrors(errors: CompilerErrorInterface[]) {
145+
this.rootReporter().errors.push(...errors);
146+
}
147+
148+
public getWarnings() {
149+
return this.rootReporter().warnings;
150+
}
151+
152+
public addWarnings(warnings: SyntaxErrorInterface[]) {
153+
this.rootReporter().warnings.push(...warnings);
154+
}
155+
140156
protected rootReporter(): ErrorReporter {
141157
return this.parent ? this.parent.rootReporter() : this;
142158
}

packages/cubejs-schema-compiler/src/compiler/PrepareCompiler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export const prepareCompiler = (repo: SchemaFileRepository, options: PrepareComp
6969
contextCompilers: [contextEvaluator],
7070
cubeFactory: cubeSymbols.createCube.bind(cubeSymbols),
7171
compilerCache,
72+
cubeDictionary,
73+
cubeSymbols,
7274
extensions: {
7375
Funnels,
7476
RefreshKeys,

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import * as t from '@babel/types';
22
import R from 'ramda';
33

44
import type { NodePath } from '@babel/traverse';
5-
import type { TranspilerInterface, TraverseObject } from './transpiler.interface';
5+
import {
6+
TranspilerCubeResolver,
7+
TranspilerInterface,
8+
TranspilerSymbolResolver,
9+
TraverseObject
10+
} from './transpiler.interface';
611
import type { CubeSymbols } from '../CubeSymbols';
712
import type { CubeDictionary } from '../CubeDictionary';
813

@@ -39,9 +44,9 @@ transpiledFieldsPatterns?.forEach((r) => {
3944

4045
export class CubePropContextTranspiler implements TranspilerInterface {
4146
public constructor(
42-
protected readonly cubeSymbols: CubeSymbols,
43-
protected readonly cubeDictionary: CubeDictionary,
44-
protected readonly viewCompiler: CubeSymbols,
47+
protected readonly cubeSymbols: TranspilerSymbolResolver,
48+
protected readonly cubeDictionary: TranspilerCubeResolver,
49+
protected readonly viewCompiler: TranspilerSymbolResolver,
4550
) {
4651
}
4752

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TranspilerCubeResolver } from './transpiler.interface';
2+
3+
export class LightweightNodeCubeDictionary implements TranspilerCubeResolver {
4+
public constructor(private cubeNames: string[] = []) {
5+
}
6+
7+
public resolveCube(name: string): boolean {
8+
return this.cubeNames.includes(name);
9+
}
10+
11+
public setCubeNames(cubeNames: string[]): void {
12+
this.cubeNames = cubeNames;
13+
}
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { TranspilerSymbolResolver } from './transpiler.interface';
2+
import { CONTEXT_SYMBOLS, CURRENT_CUBE_CONSTANTS } from '../CubeSymbols';
3+
4+
type CubeSymbols = Record<string, Record<string, boolean>>;
5+
6+
export class LightweightSymbolResolver implements TranspilerSymbolResolver {
7+
public constructor(private symbols: CubeSymbols = {}) {
8+
}
9+
10+
public setSymbols(symbols: CubeSymbols) {
11+
this.symbols = symbols;
12+
}
13+
14+
public isCurrentCube(name): boolean {
15+
return CURRENT_CUBE_CONSTANTS.indexOf(name) >= 0;
16+
}
17+
18+
public resolveSymbol(cubeName, name): any {
19+
if (name === 'USER_CONTEXT') {
20+
throw new Error('Support for USER_CONTEXT was removed, please migrate to SECURITY_CONTEXT.');
21+
}
22+
23+
if (CONTEXT_SYMBOLS[name]) {
24+
return true;
25+
}
26+
27+
const cube = this.symbols[this.isCurrentCube(name) ? cubeName : name];
28+
return cube || (this.symbols[cubeName] && this.symbols[cubeName][name]);
29+
}
30+
}

0 commit comments

Comments
 (0)