NETSCRIPT: Rework script ram updates (#408)

This commit is contained in:
Snarling 2023-03-05 22:39:42 -05:00 committed by GitHub
parent 14aafbe0a3
commit 759f86d6e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 171 additions and 177 deletions

@ -1,4 +1,4 @@
import { PlayerObject } from "src/PersonObjects/Player/PlayerObject"; import { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { SkillNames } from "../Bladeburner/data/SkillNames"; import { SkillNames } from "../Bladeburner/data/SkillNames";
import { Skills } from "../Bladeburner/Skills"; import { Skills } from "../Bladeburner/Skills";
@ -341,7 +341,7 @@ export const achievements: Record<string, Achievement> = {
SCRIPT_32GB: { SCRIPT_32GB: {
...achievementData["SCRIPT_32GB"], ...achievementData["SCRIPT_32GB"],
Icon: "bigcost", Icon: "bigcost",
Condition: () => Player.getHomeComputer().scripts.some((s) => s.ramUsage >= 32), Condition: () => Player.getHomeComputer().scripts.some((s) => (s.ramUsage as number) >= 32),
}, },
FIRST_HACKNET_NODE: { FIRST_HACKNET_NODE: {
...achievementData["FIRST_HACKNET_NODE"], ...achievementData["FIRST_HACKNET_NODE"],

@ -33,6 +33,7 @@ import { BaseServer } from "../Server/BaseServer";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { checkEnum } from "../utils/helpers/enum"; import { checkEnum } from "../utils/helpers/enum";
import { RamCostConstants } from "./RamCostGenerator"; import { RamCostConstants } from "./RamCostGenerator";
import { PositiveInteger } from "../types";
export const helpers = { export const helpers = {
string, string,
@ -158,12 +159,12 @@ function number(ctx: NetscriptContext, argName: string, v: unknown): number {
} }
/** Convert provided value v for argument argName to a positive integer, throwing if it looks like something else. */ /** Convert provided value v for argument argName to a positive integer, throwing if it looks like something else. */
function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): number { function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveInteger {
const n = number(ctx, argName, v); const n = number(ctx, argName, v);
if (n < 1 || !Number.isInteger(n)) { if (n < 1 || !Number.isInteger(n)) {
throw makeRuntimeErrorMsg(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE"); throw makeRuntimeErrorMsg(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE");
} }
return n; return n as PositiveInteger;
} }
/** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */ /** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */
@ -348,11 +349,6 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
if (ws.dynamicLoadedFns[fnName]) return; if (ws.dynamicLoadedFns[fnName]) return;
ws.dynamicLoadedFns[fnName] = true; ws.dynamicLoadedFns[fnName] = true;
let threads = ws.scriptRef.threads;
if (typeof threads !== "number") {
console.warn(`WorkerScript detected NaN for thread count for ${ws.name} on ${ws.hostname}`);
threads = 1;
}
ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max); ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max);
if (ws.dynamicRamUsage > 1.01 * ws.ramUsage) { if (ws.dynamicRamUsage > 1.01 * ws.ramUsage) {
log(ctx, () => "Insufficient static ram available."); log(ctx, () => "Insufficient static ram available.");
@ -362,7 +358,7 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
`Dynamic RAM usage calculated to be greater than initial RAM usage. `Dynamic RAM usage calculated to be greater than initial RAM usage.
This is probably because you somehow circumvented the static RAM calculation. This is probably because you somehow circumvented the static RAM calculation.
Threads: ${threads} Threads: ${ws.scriptRef.threads}
Dynamic RAM Usage: ${formatRam(ws.dynamicRamUsage)} per thread Dynamic RAM Usage: ${formatRam(ws.dynamicRamUsage)} per thread
Static RAM Usage: ${formatRam(ws.ramUsage)} per thread Static RAM Usage: ${formatRam(ws.ramUsage)} per thread

@ -72,7 +72,7 @@ export class WorkerScript {
pid: number; pid: number;
/** Script's Static RAM usage. Equivalent to underlying script's RAM usage */ /** Script's Static RAM usage. Equivalent to underlying script's RAM usage */
ramUsage = 0; ramUsage = RamCostConstants.Base;
/** Reference to underlying RunningScript object */ /** Reference to underlying RunningScript object */
scriptRef: RunningScript; scriptRef: RunningScript;

@ -36,7 +36,7 @@ import {
} from "./Server/ServerPurchases"; } from "./Server/ServerPurchases";
import { Server } from "./Server/Server"; import { Server } from "./Server/Server";
import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing";
import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { areFilesEqual, isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { TextFile, getTextFile, createTextFile } from "./TextFile"; import { TextFile, getTextFile, createTextFile } from "./TextFile";
import { runScriptFromScript } from "./NetscriptWorker"; import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript"; import { killWorkerScript } from "./Netscript/killWorkerScript";
@ -890,7 +890,8 @@ export const ns: InternalAPI<NSFull> = {
continue; continue;
} }
destScript.code = sourceScript.code; destScript.code = sourceScript.code;
destScript.ramUsage = sourceScript.ramUsage; // Set ramUsage to null in order to force a recalculation prior to next run.
destScript.ramUsage = null;
destScript.markUpdated(); destScript.markUpdated();
helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`); helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`);
continue; continue;
@ -899,11 +900,11 @@ export const ns: InternalAPI<NSFull> = {
// Create new script if it does not already exist // Create new script if it does not already exist
const newScript = new Script(file); const newScript = new Script(file);
newScript.code = sourceScript.code; newScript.code = sourceScript.code;
newScript.ramUsage = sourceScript.ramUsage; // Set ramUsage to null in order to force a recalculation prior to next run.
newScript.ramUsage = null;
newScript.server = destServer.hostname; newScript.server = destServer.hostname;
destServer.scripts.push(newScript); destServer.scripts.push(newScript);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`);
newScript.updateRamUsage(destServer.scripts);
} }
return noFailures; return noFailures;
@ -1430,12 +1431,14 @@ export const ns: InternalAPI<NSFull> = {
let script = ctx.workerScript.getScriptOnServer(fn, server); let script = ctx.workerScript.getScriptOnServer(fn, server);
if (script == null) { if (script == null) {
// Create a new script // Create a new script
script = new Script(fn, String(data), server.hostname, server.scripts); script = new Script(fn, String(data), server.hostname);
server.scripts.push(script); server.scripts.push(script);
return script.updateRamUsage(server.scripts); return;
} }
mode === "w" ? (script.code = String(data)) : (script.code += data); mode === "w" ? (script.code = String(data)) : (script.code += data);
return script.updateRamUsage(server.scripts); // Set ram to null so a recalc is performed the next time ram usage is needed
script.ramUsage = null;
return;
} else { } else {
// Write to text file // Write to text file
if (!fn.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${fn}`); if (!fn.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${fn}`);
@ -1566,12 +1569,14 @@ export const ns: InternalAPI<NSFull> = {
const scriptname = helpers.string(ctx, "scriptname", _scriptname); const scriptname = helpers.string(ctx, "scriptname", _scriptname);
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
for (let i = 0; i < server.scripts.length; ++i) { const script = server.scripts.find((serverScript) => areFilesEqual(serverScript.filename, scriptname));
if (server.scripts[i].filename == scriptname) { if (!script) return 0;
return server.scripts[i].ramUsage; const ramUsage = script.getRamUsage(server.scripts);
} if (!ramUsage) {
helpers.log(ctx, () => `Could not calculate ram usage for ${scriptname} on ${hostname}.`);
return 0;
} }
return 0; return ramUsage;
}, },
getRunningScript: getRunningScript:
(ctx) => (ctx) =>

@ -6,6 +6,7 @@ import { Apr1Events as devMenu } from "../ui/Apr1";
import { InternalAPI } from "../Netscript/APIWrapper"; import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers } from "../Netscript/NetscriptHelpers";
import { Terminal } from "../Terminal"; import { Terminal } from "../Terminal";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
export type INetscriptExtra = { export type INetscriptExtra = {
heart: { heart: {
@ -36,7 +37,7 @@ export function NetscriptExtra(): InternalAPI<INetscriptExtra> {
real_document.completely_unused_field = undefined; real_document.completely_unused_field = undefined;
// set one to true and check that it affected the other. // set one to true and check that it affected the other.
real_document.completely_unused_field = true; real_document.completely_unused_field = true;
if (d.completely_unused_field && ctx.workerScript.ramUsage === 1.6) { if (d.completely_unused_field && ctx.workerScript.ramUsage === RamCostConstants.Base) {
Player.giveExploit(Exploit.Bypass); Player.giveExploit(Exploit.Bypass);
} }
d.completely_unused_field = undefined; d.completely_unused_field = undefined;

@ -49,6 +49,7 @@ import { canGetBonus, onExport } from "../ExportBonus";
import { saveObject } from "../SaveObject"; import { saveObject } from "../SaveObject";
import { calculateCrimeWorkStats } from "../Work/Formulas"; import { calculateCrimeWorkStats } from "../Work/Formulas";
import { findEnumMember } from "../utils/helpers/enum"; import { findEnumMember } from "../utils/helpers/enum";
import { areFilesEqual } from "../Terminal/DirectoryHelpers";
export function NetscriptSingularity(): InternalAPI<ISingularity> { export function NetscriptSingularity(): InternalAPI<ISingularity> {
const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation { const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation {
@ -77,18 +78,17 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
//Run a script after reset //Run a script after reset
if (!cbScript) return; if (!cbScript) return;
const home = Player.getHomeComputer(); const home = Player.getHomeComputer();
for (const script of home.scripts) { const script = home.scripts.find((serverScript) => areFilesEqual(serverScript.filename, cbScript));
if (script.filename === cbScript) { if (!script) return;
const ramUsage = script.ramUsage; const ramUsage = script.getRamUsage(home.scripts);
const ramAvailable = home.maxRam - home.ramUsed; if (!ramUsage) {
if (ramUsage > ramAvailable + 0.001) { return Terminal.error(`Attempted to launch ${cbScript} after reset but could not calculate ram usage.`);
return; // Not enough RAM
}
const runningScriptObj = new RunningScript(script, []); // No args
runningScriptObj.threads = 1; // Only 1 thread
startWorkerScript(runningScriptObj, home);
}
} }
const ramAvailable = home.maxRam - home.ramUsed;
if (ramUsage > ramAvailable + 0.001) return;
// Start script with no args and 1 thread (default).
const runningScriptObj = new RunningScript(script, ramUsage, []);
startWorkerScript(runningScriptObj, home);
}; };
const singularityAPI: InternalAPI<ISingularity> = { const singularityAPI: InternalAPI<ISingularity> = {

@ -28,7 +28,6 @@ export async function compile(script: Script, scripts: Script[]): Promise<Script
//If multiple compiles were called on the same script before a compilation could be completed this ensures only one compilation is actually performed. //If multiple compiles were called on the same script before a compilation could be completed this ensures only one compilation is actually performed.
if (!script.queueCompile) return script.module as Promise<ScriptModule>; if (!script.queueCompile) return script.module as Promise<ScriptModule>;
script.queueCompile = false; script.queueCompile = false;
script.updateRamUsage(scripts);
const uurls = _getScriptUrls(script, scripts, []); const uurls = _getScriptUrls(script, scripts, []);
const url = uurls[uurls.length - 1].url; const url = uurls[uurls.length - 1].url;
if (script.url && script.url !== url) URL.revokeObjectURL(script.url); if (script.url && script.url !== url) URL.revokeObjectURL(script.url);

@ -1,12 +1,13 @@
import { Settings } from "./Settings/Settings"; import { Settings } from "./Settings/Settings";
import { NetscriptPort } from "@nsdefs"; import { NetscriptPort } from "@nsdefs";
import { NetscriptPorts } from "./NetscriptWorker"; import { NetscriptPorts } from "./NetscriptWorker";
import { PositiveInteger } from "./types";
type PortData = string | number; type PortData = string | number;
type Resolver = () => void; type Resolver = () => void;
const emptyPortData = "NULL PORT DATA"; const emptyPortData = "NULL PORT DATA";
/** The object property is for typechecking and is not present at runtime */ /** The object property is for typechecking and is not present at runtime */
export type PortNumber = number & { __PortNumber: true }; export type PortNumber = PositiveInteger & { __PortNumber: true };
/** Gets the numbered port, initializing it if it doesn't already exist. /** Gets the numbered port, initializing it if it doesn't already exist.
* Only using for functions that write data/resolvers. Use NetscriptPorts.get(n) for */ * Only using for functions that write data/resolvers. Use NetscriptPorts.get(n) for */

@ -15,7 +15,6 @@ import { NetscriptFunctions } from "./NetscriptFunctions";
import { compile, Node } from "./NetscriptJSEvaluator"; import { compile, Node } from "./NetscriptJSEvaluator";
import { Port, PortNumber } from "./NetscriptPort"; import { Port, PortNumber } from "./NetscriptPort";
import { RunningScript } from "./Script/RunningScript"; import { RunningScript } from "./Script/RunningScript";
import { getRamUsageFromRunningScript } from "./Script/RunningScriptHelpers";
import { scriptCalculateOfflineProduction } from "./Script/ScriptHelpers"; import { scriptCalculateOfflineProduction } from "./Script/ScriptHelpers";
import { Script } from "./Script/Script"; import { Script } from "./Script/Script";
import { GetAllServers } from "./Server/AllServers"; import { GetAllServers } from "./Server/AllServers";
@ -32,8 +31,9 @@ import { parse } from "acorn";
import { simple as walksimple } from "acorn-walk"; import { simple as walksimple } from "acorn-walk";
import { areFilesEqual } from "./Terminal/DirectoryHelpers"; import { areFilesEqual } from "./Terminal/DirectoryHelpers";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { ScriptArg } from "./Netscript/ScriptArg"; import { ScriptArg } from "@nsdefs";
import { handleUnknownError } from "./Netscript/NetscriptHelpers"; import { handleUnknownError } from "./Netscript/NetscriptHelpers";
import { PositiveInteger } from "./types";
export const NetscriptPorts: Map<PortNumber, Port> = new Map(); export const NetscriptPorts: Map<PortNumber, Port> = new Map();
@ -53,6 +53,7 @@ async function startNetscript2Script(workerScript: WorkerScript): Promise<void>
const scripts = workerScript.getServer().scripts; const scripts = workerScript.getServer().scripts;
const script = workerScript.getScript(); const script = workerScript.getScript();
if (script === null) throw "workerScript had no associated script. This is a bug."; if (script === null) throw "workerScript had no associated script. This is a bug.";
if (!script.ramUsage) throw "Attempting to start a script with no calculated ram cost. This is a bug.";
const loadedModule = await compile(script, scripts); const loadedModule = await compile(script, scripts);
workerScript.ramUsage = script.ramUsage; workerScript.ramUsage = script.ramUsage;
const ns = workerScript.env.vars; const ns = workerScript.env.vars;
@ -294,15 +295,7 @@ export function startWorkerScript(runningScript: RunningScript, server: BaseServ
* returns {boolean} indicating whether or not the workerScript was successfully added * returns {boolean} indicating whether or not the workerScript was successfully added
*/ */
function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseServer, parent?: WorkerScript): boolean { function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseServer, parent?: WorkerScript): boolean {
// Update server's ram usage const ramUsage = roundToTwo(runningScriptObj.ramUsage * runningScriptObj.threads);
let threads = 1;
if (runningScriptObj.threads && !isNaN(runningScriptObj.threads)) {
threads = runningScriptObj.threads;
} else {
runningScriptObj.threads = 1;
}
const oneRamUsage = getRamUsageFromRunningScript(runningScriptObj);
const ramUsage = roundToTwo(oneRamUsage * threads);
const ramAvailable = server.maxRam - server.ramUsed; const ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable + 0.001) { if (ramUsage > ramAvailable + 0.001) {
dialogBoxCreate( dialogBoxCreate(
@ -327,7 +320,7 @@ function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseS
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying // Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well // RunningScript's PID as well
const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions); const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
workerScript.ramUsage = oneRamUsage; workerScript.ramUsage = runningScriptObj.ramUsage;
// Add the WorkerScript to the global pool // Add the WorkerScript to the global pool
workerScripts.set(pid, workerScript); workerScripts.set(pid, workerScript);
@ -405,82 +398,61 @@ export function loadAllRunningScripts(): void {
/** Run a script from inside another script (run(), exec(), spawn(), etc.) */ /** Run a script from inside another script (run(), exec(), spawn(), etc.) */
export function runScriptFromScript( export function runScriptFromScript(
caller: string, caller: string,
server: BaseServer, host: BaseServer,
scriptname: string, scriptname: string,
args: ScriptArg[], args: ScriptArg[],
workerScript: WorkerScript, workerScript: WorkerScript,
threads = 1, threads = 1 as PositiveInteger,
): number { ): number {
// Sanitize arguments /* Very inefficient, TODO change data structures so that finding script & checking if it's already running does
if (!(workerScript instanceof WorkerScript)) { * not require iterating through all scripts and comparing names/args. This is a big part of slowdown when
* running a large number of scripts. */
// Find the script, fail if it doesn't exist.
const script = host.scripts.find((serverScript) => areFilesEqual(serverScript.filename, scriptname));
if (!script) {
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${host.hostname}'`);
return 0; return 0;
} }
if (typeof scriptname !== "string" || !Array.isArray(args)) { // Check if script is already running on server and fail if it is.
workerScript.log(caller, () => `Invalid arguments: scriptname='${scriptname} args='${args}'`); if (host.getRunningScript(scriptname, args)) {
console.error(`runScriptFromScript() failed due to invalid arguments`); workerScript.log(caller, () => `'${scriptname}' is already running on '${host.hostname}'`);
return 0; return 0;
} }
args.forEach((arg, i) => { const singleRamUsage = script.getRamUsage(host.scripts);
if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") if (!singleRamUsage) {
throw new Error( workerScript.log(caller, () => `Ram usage could not be calculated for ${scriptname}`);
"Only strings, numbers, and booleans can be passed as arguments to other scripts.\n" +
`${scriptname} argument index ${i} is of type ${typeof arg} and value ${JSON.stringify(arg)}`,
);
});
// Check if the script is already running
const runningScriptObj = server.getRunningScript(scriptname, args);
if (runningScriptObj != null) {
workerScript.log(caller, () => `'${scriptname}' is already running on '${server.hostname}'`);
return 0; return 0;
} }
// 'null/undefined' arguments are not allowed // Check if admin rights on host, fail if not.
for (let i = 0; i < args.length; ++i) { if (host.hasAdminRights == false) {
if (args[i] == null) { workerScript.log(caller, () => `You do not have root access on '${host.hostname}'`);
workerScript.log(caller, () => "Cannot execute a script with null/undefined as an argument"); return 0;
return 0;
}
} }
// Check if the script exists and if it does run it // Calculate ram usage including thread count
for (let i = 0; i < server.scripts.length; ++i) { const ramUsage = singleRamUsage * threads;
if (!areFilesEqual(server.scripts[i].filename, scriptname)) continue;
// Check for admin rights and that there is enough RAM available to run
const script = server.scripts[i];
let ramUsage = script.ramUsage;
threads = Math.floor(Number(threads));
if (threads === 0) {
return 0;
}
ramUsage = ramUsage * threads;
const ramAvailable = server.maxRam - server.ramUsed;
if (server.hasAdminRights == false) { // Check if there is enough ram to run the script, fail if not.
workerScript.log(caller, () => `You do not have root access on '${server.hostname}'`); const ramAvailable = host.maxRam - host.ramUsed;
return 0; if (ramUsage > ramAvailable + 0.001) {
} else if (ramUsage > ramAvailable + 0.001) {
workerScript.log(
caller,
() =>
`Cannot run script '${scriptname}' (t=${threads}) on '${server.hostname}' because there is not enough available RAM!`,
);
return 0;
}
// Able to run script
workerScript.log( workerScript.log(
caller, caller,
() => `'${scriptname}' on '${server.hostname}' with ${threads} threads and args: ${arrayToString(args)}.`, () =>
`Cannot run script '${scriptname}' (t=${threads}) on '${host.hostname}' because there is not enough available RAM!`,
); );
const runningScriptObj = new RunningScript(script, args); return 0;
runningScriptObj.threads = threads;
runningScriptObj.server = server.hostname;
return startWorkerScript(runningScriptObj, server, workerScript);
} }
// Able to run script
workerScript.log(
caller,
() => `'${scriptname}' on '${host.hostname}' with ${threads} threads and args: ${arrayToString(args)}.`,
);
const runningScriptObj = new RunningScript(script, singleRamUsage, args);
runningScriptObj.threads = threads;
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${server.hostname}'`); return startWorkerScript(runningScriptObj, host, workerScript);
return 0;
} }

@ -124,8 +124,8 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg);
const script = server.getScript(fileData.filename); const script = server.getScript(fileData.filename);
if (!script) return error("File doesn't exist", msg); if (!script) return error("File doesn't exist", msg);
const ramUsage = script.ramUsage; const ramUsage = script.getRamUsage(server.scripts);
if (!ramUsage) return error("Ram cost could not be calculated", msg);
return new RFAMessage({ result: ramUsage, id: msg.id }); return new RFAMessage({ result: ramUsage, id: msg.id });
}, },

@ -10,7 +10,9 @@ import { Terminal } from "../Terminal";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { formatTime } from "../utils/helpers/formatTime"; import { formatTime } from "../utils/helpers/formatTime";
import { ScriptArg } from "../Netscript/ScriptArg"; import { ScriptArg } from "@nsdefs";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { PositiveInteger } from "../types";
export class RunningScript { export class RunningScript {
// Script arguments // Script arguments
@ -52,25 +54,24 @@ export class RunningScript {
pid = -1; pid = -1;
// How much RAM this script uses for ONE thread // How much RAM this script uses for ONE thread
ramUsage = 0; ramUsage = RamCostConstants.Base;
// hostname of the server on which this script is running // hostname of the server on which this script is running
server = ""; server = "";
// Number of threads that this script is running with // Number of threads that this script is running with
threads = 1; threads = 1 as PositiveInteger;
// Script urls for the current running script for translating urls back to file names in errors // Script urls for the current running script for translating urls back to file names in errors
dependencies: ScriptUrl[] = []; dependencies: ScriptUrl[] = [];
constructor(script: Script | null = null, args: ScriptArg[] = []) { constructor(script?: Script, ramUsage?: number, args: ScriptArg[] = []) {
if (script == null) { if (!script) return;
return; if (!ramUsage) throw new Error("Must provide a ramUsage for RunningScript initialization.");
}
this.filename = script.filename; this.filename = script.filename;
this.args = args; this.args = args;
this.server = script.server; this.server = script.server;
this.ramUsage = script.ramUsage; this.ramUsage = ramUsage;
this.dependencies = script.dependencies; this.dependencies = script.dependencies;
} }

@ -1,22 +0,0 @@
import { GetServer } from "../Server/AllServers";
import { RunningScript } from "./RunningScript";
export function getRamUsageFromRunningScript(script: RunningScript): number {
if (script.ramUsage != null && script.ramUsage > 0) {
return script.ramUsage; // Use cached value
}
const server = GetServer(script.server);
if (server == null) {
return 0;
}
for (let i = 0; i < server.scripts.length; ++i) {
if (server.scripts[i].filename === script.filename) {
// Cache the ram usage for the next call
script.ramUsage = server.scripts[i].ramUsage;
return script.ramUsage;
}
}
return 0;
}

@ -10,6 +10,7 @@ import { ScriptUrl } from "./ScriptUrl";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../utils/JSONReviver";
import { roundToTwo } from "../utils/helpers/roundToTwo"; import { roundToTwo } from "../utils/helpers/roundToTwo";
import { ScriptModule } from "./ScriptModule"; import { ScriptModule } from "./ScriptModule";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
let globalModuleSequenceNumber = 0; let globalModuleSequenceNumber = 0;
@ -41,8 +42,8 @@ export class Script {
dependencies: ScriptUrl[] = []; dependencies: ScriptUrl[] = [];
dependents: ScriptReference[] = []; dependents: ScriptReference[] = [];
// Amount of RAM this Script requires to run // Amount of RAM this Script requires to run. null indicates an error in calculating ram.
ramUsage = 0; ramUsage: number | null = null;
ramUsageEntries?: RamUsageEntry[]; ramUsageEntries?: RamUsageEntry[];
// Used to deconflict multiple simultaneous compilations. // Used to deconflict multiple simultaneous compilations.
@ -51,14 +52,11 @@ export class Script {
// hostname of server that this script is on. // hostname of server that this script is on.
server = ""; server = "";
constructor(fn = "", code = "", server = "", otherScripts: Script[] = []) { constructor(fn = "", code = "", server = "") {
this.filename = fn; this.filename = fn;
this.code = code; this.code = code;
this.server = server; // hostname of server this script is on this.server = server; // hostname of server this script is on
this.moduleSequenceNumber = ++globalModuleSequenceNumber; this.moduleSequenceNumber = ++globalModuleSequenceNumber;
if (this.code !== "") {
this.updateRamUsage(otherScripts);
}
} }
/** Download the script as a file */ /** Download the script as a file */
@ -97,14 +95,24 @@ export class Script {
this.filename = filename; this.filename = filename;
this.server = hostname; this.server = hostname;
this.updateRamUsage(otherScripts); // Null ramUsage forces a recalc next time ramUsage is needed
this.ramUsage = null;
this.markUpdated(); this.markUpdated();
for (const dependent of this.dependents) { this.dependents.forEach((dependent) => {
const [dependentScript] = otherScripts.filter( const scriptToUpdate = otherScripts.find(
(s) => s.filename === dependent.filename && s.server == dependent.server, (otherScript) => otherScript.filename === dependent.filename && otherScript.server === dependent.server,
); );
dependentScript?.markUpdated(); if (!scriptToUpdate) return;
} scriptToUpdate.ramUsage = null;
scriptToUpdate.markUpdated();
});
}
/** Gets the ram usage, while also attempting to update it if it's currently null */
getRamUsage(otherScripts: Script[]): number | null {
if (this.ramUsage) return this.ramUsage;
this.updateRamUsage(otherScripts);
return this.ramUsage;
} }
/** /**
@ -112,11 +120,11 @@ export class Script {
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/ */
updateRamUsage(otherScripts: Script[]): void { updateRamUsage(otherScripts: Script[]): void {
const res = calculateRamUsage(this.code, otherScripts); const ramCalc = calculateRamUsage(this.code, otherScripts);
if (res.cost > 0) { if (ramCalc.cost >= RamCostConstants.Base) {
this.ramUsage = roundToTwo(res.cost); this.ramUsage = roundToTwo(ramCalc.cost);
this.ramUsageEntries = res.entries; this.ramUsageEntries = ramCalc.entries;
} } else this.ramUsage = null;
this.markUpdated(); this.markUpdated();
} }

@ -272,7 +272,8 @@ export abstract class BaseServer {
if (fn === this.scripts[i].filename) { if (fn === this.scripts[i].filename) {
const script = this.scripts[i]; const script = this.scripts[i];
script.code = code; script.code = code;
script.updateRamUsage(this.scripts); // Set ramUsage to null in order to force recalculation on next run
script.ramUsage = null;
script.markUpdated(); script.markUpdated();
ret.overwritten = true; ret.overwritten = true;
ret.success = true; ret.success = true;
@ -281,7 +282,7 @@ export abstract class BaseServer {
} }
// Otherwise, create a new script // Otherwise, create a new script
const newScript = new Script(fn, code, this.hostname, this.scripts); const newScript = new Script(fn, code, this.hostname);
this.scripts.push(newScript); this.scripts.push(newScript);
ret.success = true; ret.success = true;
return ret; return ret;

@ -254,9 +254,9 @@ export function prestigeHomeComputer(homeComp: Server): void {
homeComp.programs.push(Programs.BitFlume.name); homeComp.programs.push(Programs.BitFlume.name);
} }
//Update RAM usage on all scripts //Reset RAM usage calculation for all scripts
homeComp.scripts.forEach(function (script) { homeComp.scripts.forEach(function (script) {
script.updateRamUsage(homeComp.scripts); script.ramUsage = null;
}); });
homeComp.messages.length = 0; //Remove .lit and .msg files homeComp.messages.length = 0; //Remove .lit and .msg files

@ -1,8 +1,9 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { BaseServer } from "../../Server/BaseServer";
export function mem(args: (string | number | boolean)[]): void { export function mem(args: (string | number | boolean)[], server: BaseServer): void {
try { try {
if (args.length !== 1 && args.length !== 3) { if (args.length !== 1 && args.length !== 3) {
Terminal.error("Incorrect usage of mem command. usage: mem [scriptname] [-t] [number threads]"); Terminal.error("Incorrect usage of mem command. usage: mem [scriptname] [-t] [number threads]");
@ -25,7 +26,10 @@ export function mem(args: (string | number | boolean)[]): void {
return; return;
} }
const ramUsage = script.ramUsage * numThreads; const singleRamUsage = script.getRamUsage(server.scripts);
if (!singleRamUsage) return Terminal.error(`Could not calculate ram usage for ${scriptName}`);
const ramUsage = singleRamUsage * numThreads;
Terminal.print(`This script requires ${formatRam(ramUsage)} of RAM to run for ${numThreads} thread(s)`); Terminal.print(`This script requires ${formatRam(ramUsage)} of RAM to run for ${numThreads} thread(s)`);

@ -6,6 +6,8 @@ import { RunningScript } from "../../Script/RunningScript";
import { findRunningScript } from "../../Script/ScriptHelpers"; import { findRunningScript } from "../../Script/ScriptHelpers";
import * as libarg from "arg"; import * as libarg from "arg";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
import { ScriptArg } from "@nsdefs";
import { isPositiveInteger } from "../../types";
export function runScript(commandArgs: (string | number | boolean)[], server: BaseServer): void { export function runScript(commandArgs: (string | number | boolean)[], server: BaseServer): void {
if (commandArgs.length < 1) { if (commandArgs.length < 1) {
@ -24,11 +26,12 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
}); });
const tailFlag = flags["--tail"] === true; const tailFlag = flags["--tail"] === true;
const numThreads = parseFloat(flags["-t"] ?? 1); const numThreads = parseFloat(flags["-t"] ?? 1);
if (numThreads < 1 || !Number.isInteger(numThreads)) { if (!isPositiveInteger(numThreads)) {
Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0"); return Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0");
return;
} }
const args = flags["_"];
// Todo: Switch out arg for something with typescript support
const args = flags["_"] as ScriptArg[];
// Check if this script is already running // Check if this script is already running
if (findRunningScript(scriptName, args, server) != null) { if (findRunningScript(scriptName, args, server) != null) {
@ -46,7 +49,9 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
// Check for admin rights and that there is enough RAM available to run // Check for admin rights and that there is enough RAM available to run
const script = server.scripts[i]; const script = server.scripts[i];
script.server = server.hostname; script.server = server.hostname;
const ramUsage = script.ramUsage * numThreads; const singleRamUsage = script.getRamUsage(server.scripts);
if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script.");
const ramUsage = singleRamUsage * numThreads;
const ramAvailable = server.maxRam - server.ramUsed; const ramAvailable = server.maxRam - server.ramUsed;
if (!server.hasAdminRights) { if (!server.hasAdminRights) {
@ -64,7 +69,7 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
} }
// Able to run script // Able to run script
const runningScript = new RunningScript(script, args); const runningScript = new RunningScript(script, singleRamUsage, args);
runningScript.threads = numThreads; runningScript.threads = numThreads;
const success = startWorkerScript(runningScript, server); const success = startWorkerScript(runningScript, server);

@ -1,6 +1,5 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { getRamUsageFromRunningScript } from "../../Script/RunningScriptHelpers";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
export function top(args: (string | number | boolean)[], server: BaseServer): void { export function top(args: (string | number | boolean)[], server: BaseServer): void {
@ -45,7 +44,7 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo
const spacesThread = " ".repeat(numSpacesThread); const spacesThread = " ".repeat(numSpacesThread);
// Calculate and transform RAM usage // Calculate and transform RAM usage
const ramUsage = formatRam(getRamUsageFromRunningScript(script) * script.threads); const ramUsage = formatRam(script.ramUsage * script.threads);
const entry = [script.filename, spacesScript, script.pid, spacesPid, script.threads, spacesThread, ramUsage].join( const entry = [script.filename, spacesScript, script.pid, spacesPid, script.threads, spacesThread, ramUsage].join(
"", "",

@ -1,4 +1,4 @@
import { Person } from "src/PersonObjects/Person"; import { Person } from "../PersonObjects/Person";
import { Player } from "@player"; import { Player } from "@player";
import { Multipliers } from "../PersonObjects/Multipliers"; import { Multipliers } from "../PersonObjects/Multipliers";

@ -1,3 +1,12 @@
// The object properties on these numeric types are for typechecking only and do not exist at runtime. They're just a way to require a typechecking function.
export type Integer = number & { __Integer: true };
export type PositiveNumber = number & { __Positive: true };
export type PositiveInteger = Integer & PositiveNumber;
// Numeric typechecking functions
export const isInteger = (n: unknown): n is Integer => Number.isInteger(n);
export const isPositiveInteger = (n: unknown): n is PositiveInteger => isInteger(n) && n > 0;
/** Status object for functions that return a boolean indicating success/failure /** Status object for functions that return a boolean indicating success/failure
* and an optional message */ * and an optional message */
export interface IReturnStatus { export interface IReturnStatus {

@ -20,6 +20,8 @@ import { Settings } from "../../Settings/Settings";
import { ANSIITypography } from "./ANSIITypography"; import { ANSIITypography } from "./ANSIITypography";
import { ScriptArg } from "../../Netscript/ScriptArg"; import { ScriptArg } from "../../Netscript/ScriptArg";
import { useRerender } from "./hooks"; import { useRerender } from "./hooks";
import { areFilesEqual } from "../../Terminal/DirectoryHelpers";
import { dialogBoxCreate } from "./DialogBox";
let layerCounter = 0; let layerCounter = 0;
@ -220,7 +222,17 @@ function LogWindow(props: IProps): React.ReactElement {
if (server === null) return; if (server === null) return;
const s = findRunningScript(script.filename, script.args, server); const s = findRunningScript(script.filename, script.args, server);
if (s === null) { if (s === null) {
script.ramUsage = 0; const baseScript = server.scripts.find((serverScript) => areFilesEqual(serverScript.filename, script.filename));
if (!baseScript) {
return dialogBoxCreate(
`Could not launch script. The script ${script.filename} no longer exists on the server ${server.hostname}.`,
);
}
const ramUsage = baseScript.getRamUsage(server.scripts);
if (!ramUsage) {
return dialogBoxCreate(`Could not calculate ram usage for ${script.filename} on ${server.hostname}.`);
}
script.ramUsage = ramUsage;
startWorkerScript(script, server); startWorkerScript(script, server);
} else { } else {
setScript(s); setScript(s);

@ -2,7 +2,7 @@ import * as React from "react";
import LinearProgress from "@mui/material/LinearProgress"; import LinearProgress from "@mui/material/LinearProgress";
import { TableCell, Tooltip, Typography } from "@mui/material"; import { TableCell, Tooltip, Typography } from "@mui/material";
import { characterOverviewStyles } from "./CharacterOverview"; import { characterOverviewStyles } from "./CharacterOverview";
import { ISkillProgress } from "src/PersonObjects/formulas/skill"; import { ISkillProgress } from "../../PersonObjects/formulas/skill";
import { formatExp } from "../formatNumber"; import { formatExp } from "../formatNumber";
interface IProgressProps { interface IProgressProps {

@ -36,8 +36,11 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
/** Creates a RunningScript object which calculates static ram usage */ /** Creates a RunningScript object which calculates static ram usage */
function createRunningScript(code: string) { function createRunningScript(code: string) {
script.code = code; script.code = code;
script.updateRamUsage([]); // Force ram calculation reset
const runningScript = new RunningScript(script); script.ramUsage = null;
const ramUsage = script.getRamUsage([]);
if (!ramUsage) throw new Error("Ram usage should be defined.");
const runningScript = new RunningScript(script, ramUsage);
return runningScript; return runningScript;
} }

@ -52,7 +52,7 @@ test.each([
const alerted = new Promise((resolve) => { const alerted = new Promise((resolve) => {
alertDelete = AlertEvents.subscribe((x) => resolve(x)); alertDelete = AlertEvents.subscribe((x) => resolve(x));
}); });
server = new Server({ hostname: "home", hasAdminRights: true, maxRam: 8 }); server = new Server({ hostname: "home", adminRights: true, maxRam: 8 });
AddToAllServers(server); AddToAllServers(server);
for (const s of scripts) { for (const s of scripts) {
expect(server.writeToScriptFile(s.name, s.code)).toEqual({ success: true, overwritten: false }); expect(server.writeToScriptFile(s.name, s.code)).toEqual({ success: true, overwritten: false });
@ -61,8 +61,8 @@ test.each([
const script = server.scripts[server.scripts.length - 1]; const script = server.scripts[server.scripts.length - 1];
expect(script.filename).toEqual(scripts[scripts.length - 1].name); expect(script.filename).toEqual(scripts[scripts.length - 1].name);
const runningScript = new RunningScript(script); const ramUsage = script.getRamUsage(server.scripts);
runningScript.threads = 1; const runningScript = new RunningScript(script, ramUsage as number);
expect(startWorkerScript(runningScript, server)).toBeGreaterThan(0); expect(startWorkerScript(runningScript, server)).toBeGreaterThan(0);
// We don't care about start, so subscribe after that. Await script death. // We don't care about start, so subscribe after that. Await script death.
const result = await Promise.race([ const result = await Promise.race([