mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-18 20:25:45 +01:00
FIX: show ram calculation error reason to player (#627)
This commit is contained in:
parent
9e75621cd2
commit
08e3afd125
@ -22,10 +22,21 @@ export interface RamUsageEntry {
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface RamCalculation {
|
||||
export type RamCalculationSuccess = {
|
||||
cost: number;
|
||||
entries?: RamUsageEntry[];
|
||||
}
|
||||
entries: RamUsageEntry[];
|
||||
errorCode?: never;
|
||||
errorMessage?: never;
|
||||
};
|
||||
|
||||
export type RamCalculationFailure = {
|
||||
cost?: never;
|
||||
entries?: never;
|
||||
errorCode: RamCalculationErrorCode;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export type RamCalculation = RamCalculationSuccess | RamCalculationFailure;
|
||||
|
||||
// These special strings are used to reference the presence of a given logical
|
||||
// construct within a user script.
|
||||
@ -80,115 +91,112 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code:
|
||||
dependencyMap = Object.assign(dependencyMap, result.dependencyMap);
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the initial module, which is the "main" script that is being run
|
||||
const initialModule = "__SPECIAL_INITIAL_MODULE__";
|
||||
parseCode(code, initialModule);
|
||||
// Parse the initial module, which is the "main" script that is being run
|
||||
const initialModule = "__SPECIAL_INITIAL_MODULE__";
|
||||
parseCode(code, initialModule);
|
||||
|
||||
// Process additional modules, which occurs if the "main" script has any imports
|
||||
while (parseQueue.length > 0) {
|
||||
const nextModule = parseQueue.shift();
|
||||
if (nextModule === undefined) throw new Error("nextModule should not be undefined");
|
||||
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
|
||||
// Process additional modules, which occurs if the "main" script has any imports
|
||||
while (parseQueue.length > 0) {
|
||||
const nextModule = parseQueue.shift();
|
||||
if (nextModule === undefined) throw new Error("nextModule should not be undefined");
|
||||
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
|
||||
|
||||
// Using root as the path base right now. Difficult to implement
|
||||
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
|
||||
if (!filename) return { cost: RamCalculationErrorCode.ImportError }; // Invalid import path
|
||||
const script = otherScripts.get(filename);
|
||||
if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server
|
||||
|
||||
parseCode(script.code, nextModule);
|
||||
// Using root as the path base right now. Difficult to implement
|
||||
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
|
||||
if (!filename) {
|
||||
return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `Invalid import path: "${nextModule}"` };
|
||||
}
|
||||
const script = otherScripts.get(filename);
|
||||
if (!script) {
|
||||
return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `No such file on server: "${filename}"` };
|
||||
}
|
||||
|
||||
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
|
||||
// are those that start with __SPECIAL_INITIAL_MODULE__.
|
||||
let ram = RamCostConstants.Base;
|
||||
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
|
||||
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
|
||||
const resolvedRefs = new Set();
|
||||
const loadedFns: Record<string, boolean> = {};
|
||||
while (unresolvedRefs.length > 0) {
|
||||
const ref = unresolvedRefs.shift();
|
||||
if (ref === undefined) throw new Error("ref should not be undefined");
|
||||
parseCode(script.code, nextModule);
|
||||
}
|
||||
|
||||
// Check if this is one of the special keys, and add the appropriate ram cost if so.
|
||||
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
|
||||
ram += RamCostConstants.HacknetNodes;
|
||||
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes });
|
||||
}
|
||||
if (ref === "document" && !resolvedRefs.has("document")) {
|
||||
ram += RamCostConstants.Dom;
|
||||
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom });
|
||||
}
|
||||
if (ref === "window" && !resolvedRefs.has("window")) {
|
||||
ram += RamCostConstants.Dom;
|
||||
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom });
|
||||
}
|
||||
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
|
||||
// are those that start with __SPECIAL_INITIAL_MODULE__.
|
||||
let ram = RamCostConstants.Base;
|
||||
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
|
||||
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
|
||||
const resolvedRefs = new Set();
|
||||
const loadedFns: Record<string, boolean> = {};
|
||||
while (unresolvedRefs.length > 0) {
|
||||
const ref = unresolvedRefs.shift();
|
||||
if (ref === undefined) throw new Error("ref should not be undefined");
|
||||
|
||||
resolvedRefs.add(ref);
|
||||
// Check if this is one of the special keys, and add the appropriate ram cost if so.
|
||||
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
|
||||
ram += RamCostConstants.HacknetNodes;
|
||||
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes });
|
||||
}
|
||||
if (ref === "document" && !resolvedRefs.has("document")) {
|
||||
ram += RamCostConstants.Dom;
|
||||
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom });
|
||||
}
|
||||
if (ref === "window" && !resolvedRefs.has("window")) {
|
||||
ram += RamCostConstants.Dom;
|
||||
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom });
|
||||
}
|
||||
|
||||
if (ref.endsWith(".*")) {
|
||||
// A prefix reference. We need to find all matching identifiers.
|
||||
const prefix = ref.slice(0, ref.length - 2);
|
||||
for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) {
|
||||
for (const dep of dependencyMap[ident] || []) {
|
||||
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// An exact reference. Add all dependencies of this ref.
|
||||
for (const dep of dependencyMap[ref] || []) {
|
||||
resolvedRefs.add(ref);
|
||||
|
||||
if (ref.endsWith(".*")) {
|
||||
// A prefix reference. We need to find all matching identifiers.
|
||||
const prefix = ref.slice(0, ref.length - 2);
|
||||
for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) {
|
||||
for (const dep of dependencyMap[ident] || []) {
|
||||
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this identifier is a function in the workerScript environment.
|
||||
// If it is, then we need to get its RAM cost.
|
||||
try {
|
||||
// Only count each function once
|
||||
if (loadedFns[ref]) {
|
||||
continue;
|
||||
}
|
||||
loadedFns[ref] = true;
|
||||
|
||||
// This accounts for namespaces (Bladeburner, CodingContract, etc.)
|
||||
const findFunc = (
|
||||
prefix: string,
|
||||
obj: object,
|
||||
ref: string,
|
||||
): { func: () => number | number; refDetail: string } | undefined => {
|
||||
if (!obj) return;
|
||||
const elem = Object.entries(obj).find(([key]) => key === ref);
|
||||
if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) {
|
||||
return { func: elem[1], refDetail: `${prefix}${ref}` };
|
||||
}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const found = findFunc(`${key}.`, value, ref);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const details = findFunc("", RamCosts, ref);
|
||||
const fnRam = getNumericCost(details?.func ?? 0);
|
||||
ram += fnRam;
|
||||
detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
continue;
|
||||
} else {
|
||||
// An exact reference. Add all dependencies of this ref.
|
||||
for (const dep of dependencyMap[ref] || []) {
|
||||
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
|
||||
}
|
||||
}
|
||||
if (ram > RamCostConstants.Max) {
|
||||
ram = RamCostConstants.Max;
|
||||
detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max });
|
||||
|
||||
// Check if this identifier is a function in the workerScript environment.
|
||||
// If it is, then we need to get its RAM cost.
|
||||
try {
|
||||
// Only count each function once
|
||||
if (loadedFns[ref]) {
|
||||
continue;
|
||||
}
|
||||
loadedFns[ref] = true;
|
||||
|
||||
// This accounts for namespaces (Bladeburner, CodingContract, etc.)
|
||||
const findFunc = (
|
||||
prefix: string,
|
||||
obj: object,
|
||||
ref: string,
|
||||
): { func: () => number | number; refDetail: string } | undefined => {
|
||||
if (!obj) return;
|
||||
const elem = Object.entries(obj).find(([key]) => key === ref);
|
||||
if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) {
|
||||
return { func: elem[1], refDetail: `${prefix}${ref}` };
|
||||
}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const found = findFunc(`${key}.`, value, ref);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const details = findFunc("", RamCosts, ref);
|
||||
const fnRam = getNumericCost(details?.func ?? 0);
|
||||
ram += fnRam;
|
||||
detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
continue;
|
||||
}
|
||||
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
|
||||
} catch (error) {
|
||||
// console.info("parse or eval error: ", error);
|
||||
// This is not unexpected. The user may be editing a script, and it may be in
|
||||
// a transitory invalid state.
|
||||
return { cost: RamCalculationErrorCode.SyntaxError };
|
||||
}
|
||||
if (ram > RamCostConstants.Max) {
|
||||
ram = RamCostConstants.Max;
|
||||
detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max });
|
||||
}
|
||||
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
|
||||
}
|
||||
|
||||
export function checkInfiniteLoop(code: string): number {
|
||||
@ -362,20 +370,21 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
|
||||
|
||||
/**
|
||||
* Calculate's a scripts RAM Usage
|
||||
* @param {string} codeCopy - The script's code
|
||||
* @param {string} code - The script's code
|
||||
* @param {Script[]} otherScripts - All other scripts on the server.
|
||||
* Used to account for imported scripts
|
||||
*/
|
||||
export function calculateRamUsage(
|
||||
codeCopy: string,
|
||||
code: string,
|
||||
otherScripts: Map<ScriptFilePath, Script>,
|
||||
ns1?: boolean,
|
||||
): RamCalculation {
|
||||
try {
|
||||
return parseOnlyRamCalculate(otherScripts, codeCopy, ns1);
|
||||
return parseOnlyRamCalculate(otherScripts, code, ns1);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse script for RAM calculations:`);
|
||||
console.error(e);
|
||||
return { cost: RamCalculationErrorCode.SyntaxError };
|
||||
return {
|
||||
errorCode: RamCalculationErrorCode.SyntaxError,
|
||||
errorMessage: e instanceof Error ? e.message : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export class Script implements ContentFile {
|
||||
// Ram calculation, only exists after first poll of ram cost after updating
|
||||
ramUsage: number | null = null;
|
||||
ramUsageEntries: RamUsageEntry[] = [];
|
||||
ramCalculationError: string | null = null;
|
||||
|
||||
// Runtime data that only exists when the script has been initiated. Cleared when script or a dependency script is updated.
|
||||
mod: LoadedModule | null = null;
|
||||
@ -65,6 +66,7 @@ export class Script implements ContentFile {
|
||||
// Always clear ram usage
|
||||
this.ramUsage = null;
|
||||
this.ramUsageEntries.length = 0;
|
||||
this.ramCalculationError = null;
|
||||
// Early return if there's already no URL
|
||||
if (!this.mod) return;
|
||||
this.mod = null;
|
||||
@ -88,12 +90,15 @@ export class Script implements ContentFile {
|
||||
*/
|
||||
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void {
|
||||
const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script"));
|
||||
if (ramCalc.cost >= RamCostConstants.Base) {
|
||||
if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) {
|
||||
this.ramUsage = roundToTwo(ramCalc.cost);
|
||||
this.ramUsageEntries = ramCalc.entries as RamUsageEntry[];
|
||||
} else {
|
||||
this.ramUsage = null;
|
||||
this.ramCalculationError = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.ramUsage = null;
|
||||
this.ramCalculationError = ramCalc.errorMessage ?? null;
|
||||
}
|
||||
|
||||
/** Remove script from server. Fails if the provided server isn't the server for this script. */
|
||||
|
@ -34,9 +34,9 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React
|
||||
setRamEntries([["N/A", ""]]);
|
||||
return;
|
||||
}
|
||||
const codeCopy = newCode + "";
|
||||
const ramUsage = calculateRamUsage(codeCopy, server.scripts);
|
||||
if (ramUsage.cost > 0) {
|
||||
|
||||
const ramUsage = calculateRamUsage(newCode, server.scripts);
|
||||
if (ramUsage.cost && ramUsage.cost > 0) {
|
||||
const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? [];
|
||||
const entriesDisp = [];
|
||||
for (const entry of entries) {
|
||||
@ -48,23 +48,20 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React
|
||||
return;
|
||||
}
|
||||
|
||||
let RAM = "";
|
||||
const entriesDisp = [];
|
||||
switch (ramUsage.cost) {
|
||||
case RamCalculationErrorCode.ImportError: {
|
||||
RAM = "RAM: Import Error";
|
||||
entriesDisp.push(["Import Error", ""]);
|
||||
break;
|
||||
}
|
||||
case RamCalculationErrorCode.SyntaxError:
|
||||
default: {
|
||||
RAM = "RAM: Syntax Error";
|
||||
entriesDisp.push(["Syntax Error", ""]);
|
||||
break;
|
||||
if (ramUsage.errorCode !== undefined) {
|
||||
setRamEntries([["Syntax Error", ramUsage.errorMessage ?? ""]]);
|
||||
switch (ramUsage.errorCode) {
|
||||
case RamCalculationErrorCode.ImportError:
|
||||
setRAM("RAM: Import Error");
|
||||
break;
|
||||
case RamCalculationErrorCode.SyntaxError:
|
||||
setRAM("RAM: Syntax Error");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
setRAM("RAM: Syntax Error");
|
||||
setRamEntries([["Syntax Error", ""]]);
|
||||
}
|
||||
setRAM(RAM);
|
||||
setRamEntries(entriesDisp);
|
||||
};
|
||||
|
||||
const [isUpdatingRAM, { on: startUpdatingRAM, off: finishUpdatingRAM }] = useBoolean(false);
|
||||
|
@ -24,18 +24,19 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number |
|
||||
if (!isPositiveInteger(numThreads)) {
|
||||
return Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0");
|
||||
}
|
||||
if (!server.hasAdminRights) return Terminal.error("Need root access to run script");
|
||||
|
||||
// Todo: Switch out arg for something with typescript support
|
||||
const args = flags._ as ScriptArg[];
|
||||
|
||||
const singleRamUsage = script.getRamUsage(server.scripts);
|
||||
if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script.");
|
||||
if (!singleRamUsage) {
|
||||
return Terminal.error(`Error while calculating ram usage for this script. ${script.ramCalculationError}`);
|
||||
}
|
||||
|
||||
const ramUsage = singleRamUsage * numThreads;
|
||||
const ramAvailable = server.maxRam - server.ramUsed;
|
||||
|
||||
if (!server.hasAdminRights) return Terminal.error("Need root access to run script");
|
||||
|
||||
if (ramUsage > ramAvailable + 0.001) {
|
||||
return Terminal.error(
|
||||
"This machine does not have enough RAM to run this script" +
|
||||
|
Loading…
Reference in New Issue
Block a user