Skip to content

Commit 2eafba4

Browse files
committed
Reduce memory overhead of observable caches
These caches are using for formatting & encoding-test results, but the overhead of each cache was significant once you were holding a few tens of thousands of them. Now they're simpler (object much smaller than map) and only instantiated when actually used.
1 parent c37a64d commit 2eafba4

File tree

10 files changed

+63
-21
lines changed

10 files changed

+63
-21
lines changed

src/components/editor/content-viewer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getHeaderValue } from '../../util/headers';
1414

1515
import { ViewableContentType } from '../../model/events/content-types';
1616
import { Formatters, isEditorFormatter } from '../../model/events/body-formatting';
17+
import { ObservableCache } from '../../model/observable-cache';
1718

1819
import { ContainerSizedEditor, SelfSizedEditor } from './base-editor';
1920
import { LoadingCardContent } from '../common/loading-card';
@@ -27,7 +28,7 @@ interface ContentViewerProps {
2728
headers?: Headers;
2829
contentType: ViewableContentType;
2930
editorNode: portals.HtmlPortalNode<typeof SelfSizedEditor | typeof ContainerSizedEditor>;
30-
cache: Map<Symbol, unknown>;
31+
cache: ObservableCache;
3132

3233
// See BaseEditor.props.contentid
3334
contentId: string | null;

src/model/events/bodies.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { IObservableValue, observable, action } from 'mobx';
22

33
import { testEncodingsAsync } from '../../services/ui-worker-api';
44
import { ExchangeMessage } from '../../types';
5+
import { ObservableCache } from '../observable-cache';
56

67
const EncodedSizesCacheKey = Symbol('encoded-body-test');
78
type EncodedBodySizes = { [encoding: string]: number };
8-
type EncodedSizesCache = Map<typeof EncodedSizesCacheKey,
9-
IObservableValue<EncodedBodySizes | undefined> | undefined
10-
>;
9+
type EncodedSizesCache = ObservableCache<{
10+
[EncodedSizesCacheKey]: IObservableValue<EncodedBodySizes | undefined> | undefined
11+
}>;
1112

1213
export function testEncodings(message: ExchangeMessage): EncodedBodySizes | undefined {
1314
if (!message.body.isDecoded()) return;

src/model/events/body-formatting.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ImageViewer } from '../../components/editor/image-viewer';
1212

1313
export interface EditorFormatter {
1414
language: string;
15-
cacheKey: Symbol;
15+
cacheKey: symbol;
1616
isEditApplicable: boolean; // Can you apply this manually during editing to format an input?
1717
render(content: Buffer, headers?: Headers): string | ObservablePromise<string>;
1818
}

src/model/events/event-base.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,4 @@ export abstract class HTKEventBase {
4242
public get pinned(): boolean { return this._pinned; }
4343
public set pinned(value: boolean) { this._pinned = value; }
4444

45-
// Logic elsewhere can put values into these caches to cache calculations
46-
// about this event weakly, so they GC with the event.
47-
// Keyed by symbols only, so we know we never have conflicts.
48-
public cache = observable.map(new Map<symbol, unknown>(), { deep: false });
49-
5045
}

src/model/events/stream-message.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { computed, observable } from 'mobx';
22

33
import { InputStreamMessage } from "../../types";
44
import { asBuffer } from '../../util/buffer';
5+
import { ObservableCache } from '../observable-cache';
56

67
export class StreamMessage {
78

89
@observable
910
private inputMessage: InputStreamMessage;
1011

11-
public readonly cache = observable.map(new Map<symbol, unknown>(), { deep: false });
12+
public readonly cache = new ObservableCache();
1213

1314
constructor(
1415
inputMessage: InputStreamMessage,

src/model/http/exchange.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
getDummyResponseBreakpoint
4343
} from './exchange-breakpoint';
4444
import { UpstreamHttpExchange } from './upstream-exchange';
45+
import { ObservableCache } from '../observable-cache';
4546

4647
const HTTP_VERSIONS = [0.9, 1.0, 1.1, 2.0, 3.0] as const;
4748
export type HttpVersion = typeof HTTP_VERSIONS[number];
@@ -93,7 +94,7 @@ function addRequestMetadata(request: InputRequest): HtkRequest {
9394
: parseSource(request.headers['user-agent']),
9495
body: new HttpBody(request, request.headers),
9596
contentType: getContentType(getHeaderValue(request.headers, 'content-type')) || 'text',
96-
cache: observable.map(new Map<symbol, unknown>(), { deep: false })
97+
cache: new ObservableCache()
9798
}) as HtkRequest;
9899
} catch (e) {
99100
console.log(`Failed to parse request for ${request.url} (${request.protocol}://${request.hostname})`);
@@ -105,7 +106,7 @@ function addResponseMetadata(response: InputResponse): HtkResponse {
105106
return Object.assign(response, {
106107
body: new HttpBody(response, response.headers),
107108
contentType: getContentType(getHeaderValue(response.headers, 'content-type')) || 'text',
108-
cache: observable.map(new Map<symbol, unknown>(), { deep: false })
109+
cache: new ObservableCache()
109110
}) as HtkResponse;
110111
}
111112

@@ -340,8 +341,6 @@ export class HttpExchange extends HTKEventBase implements ViewableHttpExchange {
340341
// definitively unlinked, since some browser issues can result in exchanges not GCing immediately.
341342
// Important: for safety, this leaves the exchange in a *VALID* but reset state - not a totally blank one.
342343
cleanup() {
343-
this.cache.clear();
344-
345344
this.request.cache.clear();
346345
this.request.body.cleanup();
347346

src/model/http/upstream-exchange.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from './exchange';
2525
import { parseSource } from './sources';
2626
import { HTKEventBase } from '../events/event-base';
27+
import { ObservableCache } from '../observable-cache';
2728

2829
const upstreamRequestToUrl = (request: InputRuleEventDataMap['passthrough-request-head']): ParsedUrl => {
2930
const portString = request.port ? `:${request.port}` : '';
@@ -135,7 +136,7 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
135136
timingEvents: this.timingEvents,
136137
tags: downstreamReq.tags,
137138

138-
cache: observable.map(new Map<symbol, unknown>(), { deep: false }),
139+
cache: new ObservableCache(),
139140

140141
parsedUrl: url || downstreamReq.parsedUrl,
141142
url: url?.toString() || downstreamReq.url,
@@ -177,7 +178,7 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
177178
timingEvents: this.timingEvents,
178179
tags: this.tags,
179180

180-
cache: observable.map(new Map<symbol, unknown>(), { deep: false }),
181+
cache: new ObservableCache(),
181182
contentType: getContentType(getHeaderValue(rawHeaders, 'content-type')) || 'text',
182183

183184
statusCode,
@@ -319,8 +320,6 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
319320
}
320321

321322
cleanup() {
322-
this.cache.clear();
323-
324323
this.request.cache.clear();
325324
this.request.body.cleanup();
326325

src/model/observable-cache.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { observable } from 'mobx';
2+
3+
/**
4+
* An observable symbol-only cache, for storing calculated data
5+
* that is expensive to compute, and linking it to individual
6+
* messages (meaning messages can also actively clean it up).
7+
*/
8+
export class ObservableCache<T extends {
9+
[key: symbol]: unknown
10+
} = {
11+
[key: symbol]: unknown
12+
}> {
13+
14+
// Due to the per-message per-exchange overhead of this, we avoid actually
15+
// instantiating each cache until somebody tries to use it.
16+
#lazyInstData: T | undefined = undefined;
17+
18+
get #data(): T {
19+
if (this.#lazyInstData === undefined) {
20+
this.#lazyInstData = observable.object<{
21+
[key: symbol]: any
22+
}>({}, {}, { deep: false }) as any as T;
23+
}
24+
return this.#lazyInstData;
25+
}
26+
27+
get<K extends keyof T>(key: K): T[K] | undefined {
28+
return this.#data[key] as T[K] | undefined;
29+
}
30+
31+
set<K extends keyof T>(key: K, value: T[K]): void {
32+
this.#data[key] = value;
33+
}
34+
35+
clear(): void {
36+
for (let key of Object.getOwnPropertySymbols(this.#data)) {
37+
delete this.#data[key];
38+
}
39+
}
40+
41+
}

src/model/webrtc/rtc-connection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { action, observable, computed } from "mobx";
77
import { SelectedRTCCandidate } from "mockrtc";
8+
89
import {
910
InputRTCPeerConnected,
1011
InputRTCExternalPeerAttached,
@@ -13,6 +14,7 @@ import {
1314
} from "../../types";
1415
import { HTKEventBase } from "../events/event-base";
1516
import { parseSource } from "../http/sources";
17+
import { ObservableCache } from "../observable-cache";
1618

1719
const candidateToUrl = (candidate: SelectedRTCCandidate) =>
1820
`${candidate.protocol}://${candidate.address}:${candidate.port}`;
@@ -146,4 +148,6 @@ export class RTCConnection extends HTKEventBase {
146148
return this.closeData;
147149
}
148150

151+
readonly cache = new ObservableCache();
152+
149153
}

src/types.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import type { RTCMediaTrack } from './model/webrtc/rtc-media-track';
4848
import type { TrafficSource } from './model/http/sources';
4949
import type { EditableBody } from './model/http/editable-body';
5050
import type { ViewableContentType } from './model/events/content-types';
51+
import type { ObservableCache } from './model/observable-cache';
5152

5253
// These are the HAR types as returned from parseHar(), not the raw types as defined in the HAR itself
5354
export type HarBody = { encodedLength: number, decoded: Buffer };
@@ -181,15 +182,15 @@ export type HtkRequest = Omit<InputRequest, 'body' | 'path'> & {
181182
parsedUrl: ParsedUrl,
182183
source: TrafficSource,
183184
contentType: ViewableContentType,
184-
cache: Map<symbol, unknown>,
185+
cache: ObservableCache,
185186
body: MessageBody,
186187
trailers?: Trailers,
187188
rawTrailers?: RawTrailers
188189
};
189190

190191
export type HtkResponse = Omit<InputResponse, 'body'> & {
191192
contentType: ViewableContentType,
192-
cache: Map<symbol, unknown>,
193+
cache: ObservableCache,
193194
body: MessageBody
194195
};
195196

0 commit comments

Comments
 (0)