Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add cache for orchestrator build to reduce the re-build time #6373

Merged
merged 4 commits into from
Mar 11, 2021
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ describe('Orchestrator Tests', () => {
});

it('throws if input empty', () => {
expect(orchestratorBuilder([], nlrPath)).rejects.toThrow();
expect(orchestratorBuilder('test', [], nlrPath)).rejects.toThrow();
});

it('throws if NLR path invalid', () => {
const data: FileInfo[] = [{ name: 'hello', content: 'test', lastModified: '', path: '', relativePath: '' }];
expect(orchestratorBuilder(data, 'invalidPath')).rejects.toThrow();
expect(orchestratorBuilder('test', data, 'invalidPath')).rejects.toThrow();
});

it('produces expected snapshot and recognizer shape', async () => {
const buildOutput = await orchestratorBuilder(mockLUInput, nlrPath);
const buildOutput = await orchestratorBuilder('test', mockLUInput, nlrPath);

expect(buildOutput.outputs.map((o) => o.id)).toContain('additem.en-us.lu');

Expand All @@ -66,7 +66,7 @@ describe('Orchestrator Tests', () => {
});

it('produces expected recognizer shape', async () => {
const buildOutput = await orchestratorBuilder(mockLUInput, nlrPath);
const buildOutput = await orchestratorBuilder('test', mockLUInput, nlrPath);

expect(buildOutput.outputs.map((o) => o.id)).toContain('additem.en-us.lu');

Expand Down
15 changes: 2 additions & 13 deletions Composer/packages/server/src/models/bot/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useElectronContext } from '../../utility/electronContext';
import { COMPOSER_VERSION } from '../../constants';

import { IOrchestratorNLRList, IOrchestratorProgress, IOrchestratorSettings } from './interface';
import { OrchestratorBuilder } from './process/orchestratorBuilder';
import orchestratorBuilder from './process/orchestratorBuilder';

const crossTrainer = require('@microsoft/bf-lu/lib/parser/cross-train/crossTrainer.js');
const luBuild = require('@microsoft/bf-lu/lib/parser/lubuild/builder.js');
Expand Down Expand Up @@ -68,7 +68,6 @@ export class Builder {
snapshots: {},
},
};
private orchestratorBuilder?: OrchestratorBuilder = undefined;

public luBuilder = new luBuild.Builder((message) => {
log(message);
Expand Down Expand Up @@ -115,11 +114,6 @@ export class Builder {
await this.runOrchestratorBuild(orchestratorBuildFiles, emptyFiles);
} catch (error) {
throw new Error(error.message ?? error.text ?? 'Error publishing to LUIS or QNA.');
} finally {
if (this.orchestratorBuilder) {
this.orchestratorBuilder.exit();
this.orchestratorBuilder = undefined;
}
}
};

Expand Down Expand Up @@ -169,10 +163,6 @@ export class Builder {
public runOrchestratorBuild = async (luFiles: FileInfo[], emptyFiles: { [key: string]: boolean }) => {
if (!luFiles.filter((file) => !emptyFiles[file.name]).length) return;

if (!this.orchestratorBuilder) {
this.orchestratorBuilder = new OrchestratorBuilder();
}

const [enLuFiles, multiLangLuFiles] = partition(luFiles, (f) =>
f.name.split('.')?.[1]?.toLowerCase()?.startsWith('en')
);
Expand Down Expand Up @@ -220,8 +210,7 @@ export class Builder {
) => {
if (!luFiles.filter((file) => !emptyFiles[file.name]).length) return;
// build snapshots from LU files
if (!this.orchestratorBuilder) return;
return await this.orchestratorBuilder.build(luFiles, modelPath, this.generatedFolderPath);
return await orchestratorBuilder.build(this.botDir, luFiles, modelPath, this.generatedFolderPath);
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,26 @@ import uniqueId from 'lodash/uniqueId';
import { ResponseMsg } from './types';

class OrchestratorBuilder {
private worker: ChildProcess;
private static _worker: ChildProcess;
private resolves = {};
private rejects = {};

constructor() {
const workerScriptPath = path.join(__dirname, 'orchestratorWorker.ts');
if (fs.existsSync(workerScriptPath)) {
// set exec arguments to empty, avoid fork nodemon `--inspect` error
this.worker = fork(workerScriptPath, [], { execArgv: ['-r', 'ts-node/register'] });
} else {
// set exec arguments to empty, avoid fork nodemon `--inspect` error
this.worker = fork(path.join(__dirname, 'orchestratorWorker.js'), [], { execArgv: [] });
}
this.worker.on('message', this.handleMsg.bind(this));
OrchestratorBuilder.worker.on('message', this.handleMsg.bind(this));
}

public async build(
projectId: string,
files: FileInfo[],
modelPath: string,
generatedFolderPath: string
): Promise<{ [key: string]: string }> {
): Promise<Record<string, string>> {
const msgId = uniqueId();
const msg = { id: msgId, payload: { files, type: 'build', modelPath, generatedFolderPath } };
const msg = { id: msgId, payload: { projectId, files, type: 'build', modelPath, generatedFolderPath } };
return new Promise((resolve, reject) => {
this.resolves[msgId] = resolve;
this.rejects[msgId] = reject;
this.worker.send(msg);
OrchestratorBuilder.worker.send(msg);
});
}

Expand All @@ -56,9 +49,22 @@ class OrchestratorBuilder {
delete this.rejects[id];
}

public exit() {
this.worker.kill();
static get worker() {
if (this._worker && !this._worker.killed) {
return this._worker;
}

const workerScriptPath = path.join(__dirname, 'orchestratorWorker.ts');
if (fs.existsSync(workerScriptPath)) {
// set exec arguments to empty, avoid fork nodemon `--inspect` error
this._worker = fork(workerScriptPath, [], { execArgv: ['-r', 'ts-node/register'] });
} else {
// set exec arguments to empty, avoid fork nodemon `--inspect` error
this._worker = fork(path.join(__dirname, 'orchestratorWorker.js'), [], { execArgv: [] });
}
return this._worker;
}
}

export { OrchestratorBuilder };
const orchestratorBuilder = new OrchestratorBuilder();
export default orchestratorBuilder;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ import { IOrchestratorBuildOutput } from '../interface';

import { RequestMsg } from './types';

export class LabelResolversCache {
// use projectId to support multiple bots.
projects: Map<string, Map<string, LabelResolver>> = new Map();

public set(projectId: string, value: Map<string, LabelResolver>) {
this.projects.set(projectId, value);
}

public get(projectId: string) {
return this.projects.get(projectId) ?? new Map<string, LabelResolver>();
}

public removeProject(projectId: string) {
this.projects.delete(projectId);
}
}

const cache = new LabelResolversCache();
/**
* Orchestrator: Build command to compile .lu files into Binary LU (.blu) snapshots.
*
Expand All @@ -23,21 +41,21 @@ import { RequestMsg } from './types';
*/

export async function orchestratorBuilder(
projectId: string,
files: FileInfo[],
modelPath: string,
isDialog = true,
fullEmbedding = false
): Promise<IOrchestratorBuildOutput> {
const orchestratorLabelResolvers = new Map<string, LabelResolver>();
const orchestratorLabelResolvers = cache.get(projectId);

const luObjects = files
.filter((fi) => fi.name.endsWith('.lu') && fi.content)
.map((fi) => ({
id: fi.name,
content: fi.content,
}));

return await Orchestrator.buildAsync(
const result = await Orchestrator.buildAsync(
modelPath,
luObjects,
orchestratorLabelResolvers,
Expand All @@ -46,6 +64,8 @@ export async function orchestratorBuilder(
null,
fullEmbedding
);
cache.set(projectId, orchestratorLabelResolvers);
return result;
}

export async function writeSnapshot(output: IOrchestratorBuildOutput, generatedFolderPath: string) {
Expand All @@ -65,8 +85,8 @@ const handleMessage = async (msg: RequestMsg) => {
try {
switch (payload.type) {
case 'build': {
const { files, modelPath, generatedFolderPath } = payload;
const result = await orchestratorBuilder(files, modelPath);
const { files, modelPath, generatedFolderPath, projectId } = payload;
const result = await orchestratorBuilder(projectId, files, modelPath);
const snapshots = await writeSnapshot(result, generatedFolderPath);
process.send?.({ id: msg.id, payload: snapshots });
break;
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/server/src/models/bot/process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FileInfo } from '@bfc/shared';

export type BuildPayload = {
type: 'build';
projectId: string;
files: FileInfo[];
modelPath: string;
generatedFolderPath: string;
Expand Down