Skip to content

Commit

Permalink
Add MapOptimizer to reduce memory usage.
Browse files Browse the repository at this point in the history
  • Loading branch information
sw-joelmut committed Jul 19, 2023
1 parent 8856d24 commit fd91340
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 7 deletions.
16 changes: 14 additions & 2 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
// Licensed under the MIT License.

import React, { Fragment, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useRecoilCallback, CallbackInterface } from 'recoil';

import { Header } from './components/Header';
import { Announcement } from './components/AppComponents/Announcement';
import { MainContainer } from './components/AppComponents/MainContainer';
import { dispatcherState, userSettingsState } from './recoilModel';
import { dispatcherState, userSettingsState, lgFileState } from './recoilModel';
import { loadLocale } from './utils/fileUtil';
import { useInitializeLogger } from './telemetry/useInitializeLogger';
import { setupIcons } from './setupIcons';
import { setOneAuthEnabled } from './utils/oneAuthUtil';
import { LoadingSpinner } from './components/LoadingSpinner';
import lgWorker from './recoilModel/parsers/lgWorker';
import { LgEventType } from './recoilModel/parsers/types';

setupIcons();

Expand All @@ -34,11 +36,21 @@ export const App: React.FC = () => {
performAppCleanupOnQuit,
setMachineInfo,
} = useRecoilValue(dispatcherState);
const updateFile = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ projectId, value }) => {
callbackHelpers.set(lgFileState({ projectId, lgFileId: value.id }), value);
});

useEffect(() => {
loadLocale(appLocale);
}, [appLocale]);

useEffect(() => {
lgWorker.listen(LgEventType.OnUpdateLgFile, msg => {
const { projectId, payload } = msg.data;
updateFile({ projectId, value: payload });
})
});

useEffect(() => {
checkNodeVersion();
fetchExtensions();
Expand Down
33 changes: 33 additions & 0 deletions Composer/packages/client/src/recoilModel/parsers/lgWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Worker from './workers/lgParser.worker.ts';
import { BaseWorker } from './baseWorker';
import {
LgActionType,
LgEventType,
LgParsePayload,
LgUpdateTemplatePayload,
LgCreateTemplatePayload,
Expand All @@ -20,6 +21,38 @@ import {

// Wrapper class
class LgWorker extends BaseWorker<LgActionType> {
private listeners = new Map<LgEventType, ((msg: MessageEvent) => void)[]>();

constructor(worker: Worker) {
super(worker);

worker.onmessage = (msg) => {
const { type } = msg.data;

if (type === LgEventType.OnUpdateLgFile) {
this.listeners.get(type)?.forEach((cb) => cb(msg));
} else {
this.handleMsg(msg);
}
};
}

listen(action: LgEventType, callback: (msg: MessageEvent) => void) {
if (this.listeners.has(action)) {
this.listeners.get(action)!.push(callback);
} else {
this.listeners.set(action, [callback]);
}
}

async flush(): Promise<boolean> {
return new Promise(async (resolve) => {
this.listeners.clear();
const result = await super.flush();
resolve(result);
});
}

addProject(projectId: string) {
return this.sendMsg<LgNewCachePayload>(LgActionType.NewCache, { projectId });
}
Expand Down
4 changes: 4 additions & 0 deletions Composer/packages/client/src/recoilModel/parsers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ export enum LgActionType {
ParseAll = 'parse-all',
}

export enum LgEventType {
OnUpdateLgFile = 'on-update-lgfile',
}

export enum IndexerActionType {
Index = 'index',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { lgImportResolverGenerator, LgFile } from '@bfc/shared';

import {
LgActionType,
LgEventType,
LgParsePayload,
LgUpdateTemplatePayload,
LgCreateTemplatePayload,
Expand All @@ -16,6 +17,7 @@ import {
LgCleanCachePayload,
LgParseAllPayload,
} from '../types';
import { MapOptimizer } from '../../utils/mapOptimizer';

const ctx: Worker = self as any;

Expand Down Expand Up @@ -197,6 +199,11 @@ export const handleMessage = (msg: LgMessageEvent) => {
case LgActionType.Parse: {
const { id, content, lgFiles, projectId } = msg.payload;

const cachedFile = cache.get(projectId, id);
if (cachedFile?.isContentUnparsed === false && cachedFile?.content === content) {
return filterParseResult(cachedFile);
}

const lgFile = lgUtil.parse(id, content, lgFiles);
cache.set(projectId, lgFile);
payload = filterParseResult(lgFile);
Expand All @@ -206,12 +213,20 @@ export const handleMessage = (msg: LgMessageEvent) => {
case LgActionType.ParseAll: {
const { lgResources, projectId } = msg.payload;
// We'll do the parsing when the file is required. Save empty LG instead.
payload = lgResources.map(({ id, content }) => {
const emptyLg = emptyLgFile(id, content);
cache.set(projectId, emptyLg);
return filterParseResult(emptyLg);
payload = lgResources.map(({ id, content }) => [id, emptyLgFile(id, content)]);
const resources = new Map<string, LgFile>(payload);
cache.projects.set(projectId, resources);

const optimizer = new MapOptimizer(10, resources);
optimizer.onUpdate((_, value, ctx) => {
const refs = value.parseResult?.references?.map(({ name }) => name);
ctx.setReferences(refs);
});
optimizer.onDelete((_, value) => {
const lgFile = emptyLgFile(value.id, value.content);
cache.set(projectId, lgFile);
ctx.postMessage({ type: LgEventType.OnUpdateLgFile, projectId, payload: lgFile });
});

break;
}

Expand Down
100 changes: 100 additions & 0 deletions Composer/packages/client/src/recoilModel/utils/mapOptimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
interface MapOptimizerTree<Key> {
timestamp: number;
references: Key[];
}

interface OnUpdateMapOptimizerContext<Key> {
setReferences(references: Key[]): void;
}

export class MapOptimizer<Key, Value> {
public tree = new Map<Key, MapOptimizerTree<Key>>();
private skipOptimize = new Set<Key>();

opUpdateCallback?: (key: Key, value: Value, ctx: OnUpdateMapOptimizerContext<Key>) => void;
onDeleteCallback?: (key: Key, value: Value) => void;

constructor(private capacity: number, public list: Map<Key, Value>) {
this.attach();
}

onUpdate(callback: (key: Key, value: Value, ctx: OnUpdateMapOptimizerContext<Key>) => void) {
this.opUpdateCallback = callback;
}

onDelete(callback: (key: Key, value: Value) => void) {
this.onDeleteCallback = callback;
}

private attach() {
const set = this.list.set;
this.list.set = (key, value) => {
if (!this.skipOptimize.has(key)) {
this.optimize(key, value);
}
const result = set.apply(this.list, [key, value]);
return result;
};
}

private optimize(keyToAdd: Key, valueToAdd: Value) {
const exists = this.tree.has(keyToAdd);
const context: MapOptimizerTree<Key> = { timestamp: Date.now(), references: [] };
this.opUpdateCallback?.(keyToAdd, valueToAdd, {
setReferences: (references) => (context.references = references || []),
});
this.tree.set(keyToAdd, context);

if (exists) {
return;
}

let processed: [Key, MapOptimizerTree<Key>][] = [];
const itemsToRemove = Array.from(this.tree.entries())
.filter(([key]) => key !== keyToAdd)
.sort(([, v1], [, v2]) => v2.timestamp - v1.timestamp);

while (this.capacity < this.tree.size) {
const itemToRemove = itemsToRemove.pop();
if (!itemToRemove) {
break;
}

const [key, { references }] = itemToRemove;
const ids = this.identify([key, ...references]);

// Re-process previous items if an item gets deleted.
processed.push(itemToRemove);
if (ids.length > 0) {
itemsToRemove.push(...processed);
processed = [];
}

for (const id of ids) {
this.tree.delete(id);
const listItem = this.list.get(id)!;
this.skipOptimize.add(id);
this.onDeleteCallback ? this.onDeleteCallback(id, listItem) : this.list.delete(id);
this.skipOptimize.delete(id);
}
}
}

private identify(references: Key[], memo: Key[] = []) {
for (const reference of references) {
const found = this.tree.get(reference);
const existsOnMemo = () => memo.some((e) => found!.references.includes(e));
const existsOnReferences = () =>
Array.from(this.tree.values()).some(({ references }) => references.includes(reference));

if (!found || existsOnMemo() || existsOnReferences()) {
continue;
}

memo.push(reference);
this.identify(found.references, memo);
}

return memo;
}
}

0 comments on commit fd91340

Please sign in to comment.