UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

223 lines 8.5 kB
"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