NETSCRIPT: Greatly speed up script launching, and remove the limitation unique args per script (#440)

* Remove the limitation unique args per script
* Internal changes to how runningScripts are stored on the server, to make common usage faster.
This commit is contained in:
David Walker
2023-04-27 15:21:06 -07:00
committed by GitHub
parent f81297dcd6
commit aa7facd4ba
44 changed files with 573 additions and 493 deletions
+1 -2
View File
@@ -5,11 +5,10 @@ import { NSFull } from "../NetscriptFunctions";
* Netscript functions and arguments for that script.
*/
export class Environment {
/** Whether or not the script that uses this Environment should stop running */
/** Whether or not the script that uses this Environment is stopped */
stopFlag = false;
/** The currently running function */
runningFn = "";
/** Environment variables (currently only Netscript functions) */
+43 -23
View File
@@ -1,5 +1,6 @@
import { NetscriptContext } from "./APIWrapper";
import { WorkerScript } from "./WorkerScript";
import { killWorkerScript } from "./killWorkerScript";
import { GetAllServers, GetServer } from "../Server/AllServers";
import { Player } from "@player";
import { ScriptDeath } from "./ScriptDeath";
@@ -26,7 +27,7 @@ import { GangMemberTask } from "../Gang/GangMemberTask";
import { RunningScript } from "../Script/RunningScript";
import { toNative } from "../NetscriptFunctions/toNative";
import { ScriptIdentifier } from "./ScriptIdentifier";
import { findRunningScript, findRunningScriptByPid } from "../Script/ScriptHelpers";
import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelpers";
import { arrayToString } from "../utils/helpers/arrayToString";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { BaseServer } from "../Server/BaseServer";
@@ -35,6 +36,8 @@ import { checkEnum } from "../utils/helpers/enum";
import { RamCostConstants } from "./RamCostGenerator";
import { isPositiveInteger, PositiveInteger, Unknownify } from "../types";
import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
export const helpers = {
string,
@@ -61,8 +64,10 @@ export const helpers = {
gangMember,
gangTask,
log,
filePath,
scriptPath,
getRunningScript,
getRunningScriptByArgs,
getRunningScriptsByArgs,
getCannotFindRunningScriptErrorMessage,
createPublicRunningScript,
failOnHacknetServer,
@@ -73,6 +78,7 @@ export interface CompleteRunOptions {
threads: PositiveInteger;
temporary: boolean;
ramOverride?: number;
preventDuplicates: boolean;
}
export function assertMember<T extends string>(
@@ -186,6 +192,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
const result: CompleteRunOptions = {
threads: 1 as PositiveInteger,
temporary: false,
preventDuplicates: false,
};
function checkThreads(threads: unknown, argName: string) {
if (threads !== null && threads !== undefined) {
@@ -200,6 +207,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
const options = threadOrOption as Unknownify<CompleteRunOptions>;
checkThreads(options.threads, "RunOptions.threads");
result.temporary = !!options.temporary;
result.preventDuplicates = !!options.preventDuplicates;
if (options.ramOverride !== undefined && options.ramOverride !== null) {
result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride);
if (result.ramOverride < RamCostConstants.Base) {
@@ -348,10 +356,8 @@ function checkEnvFlags(ctx: NetscriptContext): void {
throw new ScriptDeath(ws);
}
if (ws.env.runningFn && ctx.function !== "asleep") {
ws.delayReject?.(new ScriptDeath(ws));
ws.env.stopFlag = true;
log(ctx, () => "Failed to run due to failed concurrency check.");
throw makeRuntimeErrorMsg(
const err = makeRuntimeErrorMsg(
ctx,
`Concurrent calls to Netscript functions are not allowed!
Did you forget to await hack(), grow(), or some other
@@ -359,6 +365,8 @@ function checkEnvFlags(ctx: NetscriptContext): void {
Currently running: ${ws.env.runningFn} tried to run: ${ctx.function}`,
"CONCURRENCY",
);
killWorkerScript(ws);
throw err;
}
}
@@ -388,8 +396,7 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max);
if (ws.dynamicRamUsage > 1.01 * ws.scriptRef.ramUsage) {
log(ctx, () => "Insufficient static ram available.");
ws.env.stopFlag = true;
throw makeRuntimeErrorMsg(
const err = makeRuntimeErrorMsg(
ctx,
`Dynamic RAM usage calculated to be greater than RAM allocation.
This is probably because you somehow circumvented the static RAM calculation.
@@ -410,6 +417,8 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
Sorry :(`,
"RAM USAGE",
);
killWorkerScript(ws);
throw err;
}
}
@@ -651,22 +660,35 @@ function log(ctx: NetscriptContext, message: () => string) {
ctx.workerScript.log(ctx.functionPath, message);
}
export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath {
assertString(ctx, argName, filename);
const path = resolveFilePath(filename, ctx.workerScript.name);
if (path) return path;
throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, was not a valid path: ${filename}`);
}
export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath {
const path = filePath(ctx, argName, filename);
if (hasScriptExtension(path)) return path;
throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, must be a script: ${filename}`);
}
/**
* Searches for and returns the RunningScript object for the specified script.
* Searches for and returns the RunningScript objects for the specified script.
* If the 'fn' argument is not specified, this returns the current RunningScript.
* @param fn - Filename of script
* @param hostname - Hostname/ip of the server on which the script resides
* @param scriptArgs - Running script's arguments
* @returns Running script identified by the parameters, or null if no such script
* exists, or the current running script if the first argument 'fn'
* @returns Running scripts identified by the parameters, or empty if no such script
* exists, or only the current running script if the first argument 'fn'
* is not specified.
*/
function getRunningScriptByArgs(
export function getRunningScriptsByArgs(
ctx: NetscriptContext,
fn: string,
hostname: string,
scriptArgs: ScriptArg[],
): RunningScript | null {
): Map<number, RunningScript> | null {
if (!Array.isArray(scriptArgs)) {
throw helpers.makeRuntimeErrorMsg(
ctx,
@@ -675,18 +697,14 @@ function getRunningScriptByArgs(
);
}
if (fn != null && typeof fn === "string") {
// Get Logs of another script
if (hostname == null) {
hostname = ctx.workerScript.hostname;
}
const server = helpers.getServer(ctx, hostname);
return findRunningScript(fn, scriptArgs, server);
const path = scriptPath(ctx, "filename", fn);
// Lookup server to scope search
if (hostname == null) {
hostname = ctx.workerScript.hostname;
}
const server = helpers.getServer(ctx, hostname);
// If no arguments are specified, return the current RunningScript
return ctx.workerScript.scriptRef;
return findRunningScripts(path, scriptArgs, server);
}
function getRunningScriptByPid(pid: number): RunningScript | null {
@@ -701,7 +719,9 @@ function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): Runni
if (typeof ident === "number") {
return getRunningScriptByPid(ident);
} else {
return getRunningScriptByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
const scripts = getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
if (scripts === null) return null;
return scripts.values().next().value;
}
}
+2 -2
View File
@@ -33,7 +33,7 @@ export class WorkerScript {
delay: number | null = null;
/** Holds the Promise reject() function while the script is "blocked" by an async op */
delayReject?: (reason?: ScriptDeath) => void;
delayReject: ((reason?: ScriptDeath) => void) | undefined = undefined;
/** Stores names of all functions that have logging disabled */
disableLogs: Record<string, boolean> = {};
@@ -79,7 +79,7 @@ export class WorkerScript {
hostname: string;
/** Function called when the script ends. */
atExit?: () => void;
atExit: (() => void) | undefined = undefined;
constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => NSFull) {
this.name = runningScriptObj.filename;
+20 -40
View File
@@ -7,47 +7,24 @@ import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
import { RunningScript } from "../Script/RunningScript";
import { GetServer } from "../Server/AllServers";
import { AddRecentScript } from "./RecentScripts";
import { ITutorial } from "../InteractiveTutorial";
import { AlertEvents } from "../ui/React/AlertManager";
import { handleUnknownError } from "./NetscriptHelpers";
import { roundToTwo } from "../utils/helpers/roundToTwo";
export type killScriptParams = WorkerScript | number | { runningScript: RunningScript; hostname: string };
export function killWorkerScript(params: killScriptParams): boolean {
export function killWorkerScript(ws: WorkerScript): boolean {
if (ITutorial.isRunning) {
AlertEvents.emit("Processes cannot be killed during the tutorial.");
return false;
}
if (params instanceof WorkerScript) {
stopAndCleanUpWorkerScript(params);
stopAndCleanUpWorkerScript(ws);
return true;
} else if (typeof params === "number") {
return killWorkerScriptByPid(params);
} else {
// Try to kill by PID
const res = killWorkerScriptByPid(params.runningScript.pid);
if (res) {
return res;
}
// If for some reason that doesn't work, we'll try the old way
for (const ws of workerScripts.values()) {
if (ws.scriptRef === params.runningScript) {
stopAndCleanUpWorkerScript(ws);
return true;
}
}
return false;
}
return true;
}
function killWorkerScriptByPid(pid: number): boolean {
export function killWorkerScriptByPid(pid: number): boolean {
const ws = workerScripts.get(pid);
if (ws instanceof WorkerScript) {
stopAndCleanUpWorkerScript(ws);
@@ -58,6 +35,10 @@ function killWorkerScriptByPid(pid: number): boolean {
}
function stopAndCleanUpWorkerScript(ws: WorkerScript): void {
// Only clean up once.
// Important: Only this function can set stopFlag!
if (ws.env.stopFlag) return;
//Clean up any ongoing netscriptDelay
if (ws.delay) clearTimeout(ws.delay);
ws.delayReject?.(new ScriptDeath(ws));
@@ -65,7 +46,6 @@ function stopAndCleanUpWorkerScript(ws: WorkerScript): void {
if (typeof ws.atExit === "function") {
try {
ws.env.stopFlag = false;
const atExit = ws.atExit;
ws.atExit = undefined;
atExit();
@@ -95,21 +75,21 @@ function removeWorkerScript(workerScript: WorkerScript): void {
}
// Delete the RunningScript object from that server
for (let i = 0; i < server.runningScripts.length; ++i) {
const runningScript = server.runningScripts[i];
if (runningScript === workerScript.scriptRef) {
server.runningScripts.splice(i, 1);
break;
const rs = workerScript.scriptRef;
const byPid = server.runningScriptMap.get(rs.scriptKey);
if (!byPid) {
console.error(`Couldn't find runningScriptMap for key ${rs.scriptKey}`);
} else {
byPid.delete(workerScript.pid);
if (byPid.size === 0) {
server.runningScriptMap.delete(rs.scriptKey);
}
}
// Recalculate ram used on that server
// Update ram used. Reround to prevent accumulation of error.
server.updateRamUsed(roundToTwo(server.ramUsed - rs.ramUsage * rs.threads));
server.updateRamUsed(0);
for (const rs of server.runningScripts) server.updateRamUsed(server.ramUsed + rs.ramUsage * rs.threads);
// Delete script from global pool (workerScripts) after verifying it's the right script (PIDs reset on aug install)
if (workerScripts.get(workerScript.pid) === workerScript) workerScripts.delete(workerScript.pid);
workerScripts.delete(workerScript.pid);
AddRecentScript(workerScript);
WorkerScriptStartStopEventEmitter.emit();
}