Skip to content

Commit ea40282

Browse files
committed
[release:patch] 2.15.4 limit environment mem consumption
1 parent e279d16 commit ea40282

20 files changed

Lines changed: 656 additions & 222 deletions

File tree

index.html

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/benchmark/stats/size-of.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ import {
1010
import sizeof from 'object-sizeof';
1111
import { compactRecord } from '../../util/objects';
1212

13+
/** the stripped copy of a frame that {@link killBuiltInEnv} hands to `sizeof`; it holds a plain map, not a frame view */
14+
interface SizedEnvironment {
15+
readonly id: number;
16+
readonly parent: SizedEnvironment;
17+
readonly memory: ReadonlyMap<BrandedIdentifier, IdentifierDefinition[]>;
18+
readonly builtInEnv?: true;
19+
}
20+
1321
/* we have to kill all processors linked in the default environment as they cannot be serialized and they are shared anyway */
14-
function killBuiltInEnv(env: IEnvironment | undefined): IEnvironment {
22+
function killBuiltInEnv(env: IEnvironment | undefined): SizedEnvironment {
1523

1624
if(env === undefined) {
17-
return undefined as unknown as IEnvironment;
25+
return undefined as unknown as SizedEnvironment;
1826
} else if(env.builtInEnv) {
1927
/* in this case, the reference would be shared for sure */
2028
return {

src/dataflow/environments/environment.ts

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { guard } from '../../util/assert';
1515
import type { ControlDependency } from '../info';
1616
import { happensInEveryBranch } from '../info';
1717
import { uniqueMergeValuesInDefinitions } from './append';
18+
import { Frame, MemoryView, WritableMemory } from './frame-memory';
1819
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id';
1920
import { log } from '../../util/log';
2021

@@ -25,7 +26,7 @@ export interface IEnvironment {
2526
/** Lexical parent of the environment, if any (can be manipulated by R code) */
2627
parent: IEnvironment
2728
/** Maps to exactly one definition of an identifier if the source is known, otherwise to a list of all possible definitions */
28-
memory: BuiltInMemory
29+
memory: MemoryView
2930
/** Built-in environment that must not change; only for the top-most envs. */
3031
builtInEnv?: true | undefined
3132
}
@@ -43,7 +44,7 @@ interface Jsonified {
4344
id: NodeId;
4445
parent: Jsonified | undefined;
4546
builtInEnv?: true;
46-
memory: BuiltInMemory;
47+
memory: ReadonlyMap<BrandedIdentifier, IdentifierDefinition[]>;
4748
n?: string;
4849
t?: EnvType;
4950
globalEnv?: true;
@@ -66,25 +67,36 @@ export class Environment implements IEnvironment {
6667
/** if created by a closure, the node id of that closure */
6768
private c?: NodeId;
6869
parent: Environment;
69-
memory: BuiltInMemory;
70+
/** the frame this environment's names live in, shared with every other snapshot of it */
71+
private frame: Frame;
72+
/** the version of {@link frame} this environment sees */
73+
private fv: number;
74+
private view?: MemoryView;
75+
private writable?: WritableMemory;
7076
cache?: Map<Identifier, IdentifierDefinition[]>;
77+
/** what a resolution found below this frame, by target and name; only frames nothing writes keep one */
78+
tailCache?: Map<string, readonly IdentifierDefinition[] | undefined>;
7179
builtInEnv?: true;
72-
/** {@link memory} is shared with a clone; writing needs {@link writableMemory} to unshare it first */
80+
/** {@link memory} may be seen by another snapshot, so a write has to take a version of its own first */
7381
private sharedMemory?: true;
7482
/** {@link parent} is shared with the environment this was cloned from; writing through it needs {@link writableParent} */
7583
private sharedParent?: true;
7684
/** marks the global environment (`.GlobalEnv`); attached packages (see {@link EnvType}) live below it */
7785
globalEnv?: true;
7886
/** What the lexical frame this stands in for held when its closure was created; `<<-` binds lexically, so defining super needs this. */
79-
superMemory?: BuiltInMemory;
87+
superMemory?: MemoryView;
8088
/** What the configuration states about packages R does not attach on startup, by package; only the built-in env carries it (see {@link statedIn}). */
8189
namespaces?: ReadonlyMap<string, BuiltInMemory>;
8290

83-
constructor(parent: Environment, isBuiltInDefault: true | undefined = undefined, memory?: BuiltInMemory) {
91+
constructor(parent: Environment, isBuiltInDefault: true | undefined = undefined, frame?: Frame, version?: number) {
8492
this.id = isBuiltInDefault ? 0 : environmentIdCounter++;
8593
this.parent = parent;
86-
/* a clone hands its own (then shared) memory in, so the fresh map a new frame needs is not allocated to be dropped */
87-
this.memory = memory ?? new Map<BrandedIdentifier, IdentifierDefinition[]>();
94+
this.frame = frame ?? Frame.empty();
95+
this.fv = version ?? this.frame.version;
96+
if(frame === undefined) {
97+
/* the shared empty frame, so the first write forks it */
98+
this.sharedMemory = true;
99+
}
88100
// do not store if not needed!
89101
if(isBuiltInDefault) {
90102
this.builtInEnv = isBuiltInDefault;
@@ -105,7 +117,7 @@ export class Environment implements IEnvironment {
105117
}
106118

107119
/** Records the lexical frame this one stands in for; see {@link superMemory}. */
108-
public standsInFor(memory: BuiltInMemory): this {
120+
public standsInFor(memory: MemoryView): this {
109121
this.superMemory = memory;
110122
return this;
111123
}
@@ -134,16 +146,48 @@ export class Environment implements IEnvironment {
134146
return this.parent;
135147
}
136148

137-
/**
138-
* This environment's {@link memory}, ready to be written to; every in-place write must go through this rather
139-
* than {@link memory} directly, since {@link clone} shares the map and only the first writer copies it.
140-
*/
141-
public get writableMemory(): BuiltInMemory {
149+
/** What the frame binds at the version this sees; a snapshot, not a copy. */
150+
public get memory(): MemoryView {
151+
return this.view ??= new MemoryView(this.frame, this.fv);
152+
}
153+
154+
/** Whether `other` holds the very bindings this does. */
155+
public sameMemoryAs(other: Environment): boolean {
156+
return this === other || (this.frame === other.frame && this.fv === other.fv);
157+
}
158+
159+
/** What `name` holds here. {@link memory}'s answer without the view, for the chain walks of name resolution. */
160+
public lookup(name: BrandedIdentifier): IdentifierDefinition[] | undefined {
161+
const frame = this.frame;
162+
return this.fv === frame.version ? frame.live.get(name) : frame.get(name, this.fv);
163+
}
164+
165+
/** the bindings as a plain map where one is at hand, so a merge copies map from map */
166+
private get copyableMemory(): ReadonlyMap<BrandedIdentifier, IdentifierDefinition[]> {
167+
return this.frame.settledAt(this.fv) ?? this.memory;
168+
}
169+
170+
/** Takes `map` as the bindings, sharing it until something writes. */
171+
public adoptMap(map: ReadonlyMap<BrandedIdentifier, IdentifierDefinition[]>): void {
172+
this.frame = Frame.of(map);
173+
this.fv = this.frame.version;
174+
this.view = undefined;
175+
this.writable = undefined;
176+
/* the map belongs to whoever handed it over, so the first write forks */
177+
this.sharedMemory = true;
178+
}
179+
180+
/** {@link memory} ready to be written to; every write goes through this, so the first takes a version of its own. */
181+
public get writableMemory(): WritableMemory {
182+
this.tailCache = undefined;
142183
if(this.sharedMemory) {
143-
this.memory = new Map(this.memory);
184+
this.frame = this.frame.forWrite(this.fv);
185+
this.fv = this.frame.nextVersion();
144186
this.sharedMemory = undefined;
187+
this.view = undefined;
188+
this.writable = undefined;
145189
}
146-
return this.memory;
190+
return this.writable ??= new WritableMemory(this.frame, this.fv);
147191
}
148192

149193
/**
@@ -155,7 +199,7 @@ export class Environment implements IEnvironment {
155199
return this; // do not clone the built-in environment
156200
}
157201

158-
const clone = new Environment(this.parent, this.builtInEnv, this.memory);
202+
const clone = new Environment(this.parent, this.builtInEnv, this.frame, this.fv);
159203
clone.c = this.c;
160204
clone.n = this.n;
161205
clone.t = this.t;
@@ -203,7 +247,7 @@ export class Environment implements IEnvironment {
203247
if(definition.cds === undefined) {
204248
this.writableMemory.set(name, [definition]);
205249
} else {
206-
const existing = this.memory.get(name);
250+
const existing = this.lookup(name);
207251
const inGraphDefinition = definition as InGraphIdentifierDefinition;
208252
if(
209253
existing !== undefined &&
@@ -267,7 +311,7 @@ export class Environment implements IEnvironment {
267311
do{
268312
/* `<<-` binds in the closest enclosing frame that holds the name, which for an emptied frame is what
269313
* it stood in for when the closure was created (see {@link superMemory}) */
270-
if(current.memory.has(name) || current.superMemory?.has(name)) {
314+
if(current.lookup(name) !== undefined || current.superMemory?.has(name)) {
271315
current.writableMemory.set(name, [definition]);
272316
found = true;
273317
break;
@@ -298,7 +342,7 @@ export class Environment implements IEnvironment {
298342
if(shortcut !== undefined) {
299343
return shortcut;
300344
}
301-
const map = new Map(this.memory);
345+
const map = new Map(this.copyableMemory);
302346
for(const [key, values] of other.memory) {
303347
const hasMaybe = applyCds === undefined ? values.length === 0 || values.some(v => v.cds !== undefined) : true;
304348
if(hasMaybe) {
@@ -335,7 +379,7 @@ export class Environment implements IEnvironment {
335379
out.t = this.t;
336380
out.globalEnv = this.globalEnv;
337381
out.superMemory = this.superMemory ?? other.superMemory;
338-
out.memory = map;
382+
out.adoptMap(map);
339383
return out;
340384
}
341385

@@ -348,7 +392,7 @@ export class Environment implements IEnvironment {
348392
if(shortcut !== undefined) {
349393
return shortcut;
350394
}
351-
const map = new Map(this.memory);
395+
const map = new Map(this.copyableMemory);
352396
for(const [key, value] of other.memory) {
353397
const old = map.get(key);
354398
if(old) {
@@ -364,7 +408,7 @@ export class Environment implements IEnvironment {
364408
out.t = this.t;
365409
out.globalEnv = this.globalEnv;
366410
out.superMemory = this.superMemory ?? other.superMemory;
367-
out.memory = map;
411+
out.adoptMap(map);
368412
return out;
369413
}
370414

@@ -420,7 +464,7 @@ export class Environment implements IEnvironment {
420464
const cloned = layer.clone(false);
421465
byName.set(layer.n, cloned);
422466
order.push(cloned);
423-
} else if(existing.memory !== layer.memory) {
467+
} else if(!existing.sameMemoryAs(layer)) {
424468
for(const [name, value] of layer.memory) {
425469
const old = existing.memory.get(name);
426470
if(old !== value) {
@@ -443,7 +487,7 @@ export class Environment implements IEnvironment {
443487
this.writableParent.remove(id);
444488
return this;
445489
}
446-
const definition = this.memory.get(name);
490+
const definition = this.lookup(name);
447491
let cont = true;
448492
if(definition !== undefined) {
449493
this.writableMemory.delete(name);
@@ -474,11 +518,11 @@ export class Environment implements IEnvironment {
474518
id: this.id,
475519
parent: this.parent,
476520
builtInEnv: this.builtInEnv,
477-
memory: this.memory,
521+
memory: new Map(this.memory),
478522
} : {
479523
id: this.id,
480524
parent: this.parent,
481-
memory: this.memory,
525+
memory: new Map(this.memory),
482526
// markers needed to rebuild the search path after a round-trip (undefined values are dropped by JSON.stringify)
483527
n: this.n,
484528
t: this.t,
@@ -601,7 +645,7 @@ function layersEndWith(this: void, layers: readonly Environment[], tail: readonl
601645
}
602646
for(let i = 0; i < tail.length; i++) {
603647
const l = layers[offset + i], t = tail[i];
604-
if(l !== t && (l.t !== t.t || l.n !== t.n || l.memory !== t.memory)) {
648+
if(l !== t && (l.t !== t.t || l.n !== t.n || !l.sameMemoryAs(t))) {
605649
return false;
606650
}
607651
}

0 commit comments

Comments
 (0)