@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
223 lines • 8.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowrAnalyzerGasContext = void 0;
const log_1 = require("../../util/log");
let heapStatisticsProvider = undefined;
/**
* Heap statistics come from the v8 module (Node.js, Electron, VS Code extension host) or,
* as a fallback, from Chromium's non-standard performance.memory (browsers, web workers).
* If neither exists, this returns undefined and gas skips the memory check
* (use {@link FlowrGasConfig#heapProvider} or a gas plugin to supply a custom source).
*/
function tryGetHeapStatistics() {
if (heapStatisticsProvider === undefined) {
const v8 = globalThis.process?.getBuiltinModule?.('v8');
if (v8) {
heapStatisticsProvider = v8.getHeapStatistics;
}
else if (globalThis.performance?.memory) {
heapStatisticsProvider = () => {
const m = globalThis.performance.memory;
return { used_heap_size: m.usedJSHeapSize, heap_size_limit: m.jsHeapSizeLimit };
};
}
else {
heapStatisticsProvider = null;
log_1.log.info('no heap statistics source available in this runtime, gas skips the memory check');
}
}
return heapStatisticsProvider ? heapStatisticsProvider() : undefined;
}
function isPair(v) {
return typeof v === 'object' && v !== null;
}
/** feature entry, then `default`, then the shared pair */
function configuredBound(spec, feature, bound) {
const own = spec?.[feature];
if (isPair(own) && own[bound] !== undefined) {
return own[bound];
}
const fallback = spec?.default?.[bound];
if (fallback !== undefined) {
return fallback;
}
const shared = spec?.[bound];
return typeof shared === 'number' ? shared : undefined;
}
/** a bare number is an elapsed-time bound in ms */
function overriddenBound(entry, dim, bound) {
return entry?.[dim]?.[bound] ?? (dim === 'timeMs' ? entry?.[bound] : undefined);
}
function entryOf(layer, key) {
return layer[key] ?? layer.default;
}
function isThenable(v) {
return typeof v?.then === 'function';
}
/** Checks heap and elapsed-time pressure for named analysis features. See {@link ReadOnlyFlowrAnalyzerGasContext}. */
class FlowrAnalyzerGasContext {
name = 'flowr-analyzer-gas-context';
/** what a gas check falls back to when no operation declared a contingent of its own */
base;
/** the contingents of the operations in flight, innermost last */
frames = [];
config;
ctx;
plugins;
constructor(ctx, config, plugins) {
this.ctx = ctx;
this.config = config;
this.plugins = plugins;
this.base = { startTime: Date.now(), layers: [] };
}
/**
* Restart the contingent, so what follows is measured from now. Supported API: call it between phases
* that should each get the full allowance (`analyzer.context().gas.reset()`).
*
* flowR calls it itself whenever a new analysis begins, so a caller only has to split its *own* phases.
* Operations in flight keep their contingent, as restarting a running traversal's clock would defeat the
* guard bounding it.
*/
reset() {
this.base.startTime = Date.now();
}
/** A new analysis makes the spent contingent irrelevant. */
receive(_event) {
this.reset();
}
/**
* Run `fn` against a fresh contingent bounded by `overrides`, ending when it settles if `fn` is async.
* Every check while `fn` runs sees it, however deep, which is how the bounds reach sites that only ever
* get a context handed to them.
*
* Being ambient, concurrent operations on one analyzer see whichever started last, so prefer
* {@link scope} wherever the context can be threaded through.
*/
withGas(overrides, fn) {
const frame = this.derive(overrides);
this.frames.push(frame);
let result;
try {
result = fn();
}
catch (e) {
this.drop(frame);
throw e;
}
if (isThenable(result)) {
return result.then(v => {
this.drop(frame);
return v;
}, (e) => {
this.drop(frame);
throw e;
});
}
this.drop(frame);
return result;
}
scope(overrides) {
return this.viewOf(this.derive(overrides));
}
derive(overrides, from) {
const inherited = (from ?? this.activeScope()).layers;
return { startTime: Date.now(), layers: overrides ? [...inherited, overrides] : inherited };
}
viewOf(scope) {
return {
name: `${this.name}:scope`,
checkGas: key => this.levelFor(key, scope),
scope: o => this.viewOf(this.derive(o, scope))
};
}
drop(frame) {
const idx = this.frames.lastIndexOf(frame);
if (idx >= 0) {
this.frames.splice(idx, 1);
}
}
activeScope() {
return this.frames.length > 0 ? this.frames[this.frames.length - 1] : this.base;
}
/**
* The innermost layer stating this bound wins, the configured thresholds apply when none does.
* Resolved per bound, so a layer naming only `critical` leaves `problematic` to the layer outside it.
*/
bound(scope, key, dim, bound) {
for (let i = scope.layers.length - 1; i >= 0; i--) {
const stated = overriddenBound(entryOf(scope.layers[i], key), dim, bound);
if (stated !== undefined) {
return stated;
}
}
return configuredBound(this.config?.thresholds?.[dim], key, bound) ?? Number.POSITIVE_INFINITY;
}
/** An override naming the feature enables it even when the config disables it. */
factorFor(scope, key) {
const configured = this.config?.features?.[key] ?? 0;
let named = false;
for (let i = scope.layers.length - 1; i >= 0; i--) {
const entry = entryOf(scope.layers[i], key);
if (entry?.factor !== undefined) {
return entry.factor;
}
named ||= entry !== undefined;
}
return named ? configured || 1 : configured;
}
memoryLevel(factor, key, scope) {
const stats = this.config?.heapProvider ? this.config.heapProvider() : tryGetHeapStatistics();
if (stats === undefined || stats.heap_size_limit <= 0) {
return 0 /* GasLevel.Normal */;
}
const ratio = (stats.used_heap_size / stats.heap_size_limit) * factor;
if (ratio >= this.bound(scope, key, 'memory', 'critical')) {
return 2 /* GasLevel.Critical */;
}
if (ratio >= this.bound(scope, key, 'memory', 'problematic')) {
return 1 /* GasLevel.Problematic */;
}
return 0 /* GasLevel.Normal */;
}
static maxLevel(a, b) {
return a >= b ? a : b;
}
timeLevel(factor, key, scope) {
const elapsed = (Date.now() - scope.startTime) * factor;
if (elapsed >= this.bound(scope, key, 'timeMs', 'critical')) {
return 2 /* GasLevel.Critical */;
}
if (elapsed >= this.bound(scope, key, 'timeMs', 'problematic')) {
return 1 /* GasLevel.Problematic */;
}
return 0 /* GasLevel.Normal */;
}
checkGas(key) {
return this.levelFor(key, this.activeScope());
}
levelFor(key, scope) {
const factor = this.factorFor(scope, key);
if (!factor && this.plugins.length === 0) {
return 0 /* GasLevel.Normal */;
}
let level = 0 /* GasLevel.Normal */;
if (factor) {
level = FlowrAnalyzerGasContext.maxLevel(level, this.memoryLevel(factor, key, scope));
if (level < 2 /* GasLevel.Critical */) {
level = FlowrAnalyzerGasContext.maxLevel(level, this.timeLevel(factor, key, scope));
}
}
for (const plugin of this.plugins) {
if (level >= 2 /* GasLevel.Critical */) {
break;
}
const override = plugin.processor(this.ctx, key);
if (override !== undefined) {
level = FlowrAnalyzerGasContext.maxLevel(level, override);
}
}
return level;
}
}
exports.FlowrAnalyzerGasContext = FlowrAnalyzerGasContext;
//# sourceMappingURL=flowr-analyzer-gas-context.js.map