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
+3 -15
View File
@@ -10,10 +10,9 @@ import { getRandomInt } from "../utils/helpers/getRandomInt";
import { Reviver } from "../utils/JSONReviver";
import { SpecialServers } from "./data/SpecialServers";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import "../Script/RunningScript"; // For reviver side-effect
import { IPAddress, isIPAddress } from "../Types/strings";
import type { RunningScript } from "../Script/RunningScript";
import "../Script/RunningScript"; // For reviver side-effect
/**
* Map of all Servers that exist in the game
@@ -207,17 +206,6 @@ function excludeReplacer(key: string, value: any): any {
return value;
}
function scriptFilter(script: RunningScript): boolean {
return !script.temporary;
}
function includeReplacer(key: string, value: any): any {
if (key === "runningScripts") {
return value.filter(scriptFilter);
}
return value;
}
export function saveAllServers(excludeRunningScripts = false): string {
return JSON.stringify(AllServers, excludeRunningScripts ? excludeReplacer : includeReplacer);
return JSON.stringify(AllServers, excludeRunningScripts ? excludeReplacer : undefined);
}
+55 -17
View File
@@ -7,10 +7,10 @@ import { IReturnStatus } from "../types";
import { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath";
import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath";
import { Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { matchScriptPathExact } from "../utils/helpers/scriptKey";
import { createRandomIp } from "../utils/IPAddress";
import { compareArrays } from "../utils/helpers/compareArrays";
import { ScriptArg } from "../Netscript/ScriptArg";
import { JSONMap } from "../Types/Jsonable";
import { IPAddress, ServerName } from "../Types/strings";
import { FilePath } from "../Paths/FilePath";
@@ -19,6 +19,10 @@ import { ProgramFilePath, hasProgramExtension } from "../Paths/ProgramFilePath";
import { MessageFilename } from "src/Message/MessageHelpers";
import { LiteratureName } from "src/Literature/data/LiteratureNames";
import { CompletedProgramName } from "src/Programs/Programs";
import { getKeyList } from "../utils/helpers/getKeyList";
import lodash from "lodash";
import type { ScriptKey } from "../utils/helpers/scriptKey";
interface IConstructorParams {
adminRights?: boolean;
@@ -64,7 +68,7 @@ export abstract class BaseServer implements IServer {
maxRam = 0;
// Message files AND Literature files on this Server
messages: (MessageFilename | LiteratureName | FilePath)[] = [];
messages: (MessageFilename | LiteratureName)[] = [];
// Name of company/faction/etc. that this server belongs to.
// Optional, not applicable to all Servers
@@ -77,8 +81,12 @@ export abstract class BaseServer implements IServer {
// RAM (GB) used. i.e. unavailable RAM
ramUsed = 0;
// RunningScript files on this server
runningScripts: RunningScript[] = [];
// RunningScript files on this server. Keyed first by name/args, then by PID.
runningScriptMap: Map<ScriptKey, Map<number, RunningScript>> = new Map();
// RunningScript files loaded from the savegame. Only stored here temporarily,
// this field is undef while the game is running.
savedScripts: RunningScript[] | undefined = undefined;
// Script files on this Server
scripts: JSONMap<ScriptFilePath, Script> = new JSONMap();
@@ -138,23 +146,16 @@ export abstract class BaseServer implements IServer {
return null;
}
/** Find an actively running script on this server by filepath and args. */
getRunningScript(path: ScriptFilePath, scriptArgs: ScriptArg[]): RunningScript | null {
for (const rs of this.runningScripts) {
if (rs.filename === path && compareArrays(rs.args, scriptArgs)) return rs;
}
return null;
}
/** Get a TextFile or Script depending on the input path type. */
getContentFile(path: ContentFilePath): ContentFile | null {
return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? null;
}
/** Returns boolean indicating whether the given script is running on this server */
isRunning(fn: string): boolean {
for (const runningScriptObj of this.runningScripts) {
if (runningScriptObj.filename === fn) {
isRunning(path: ScriptFilePath): boolean {
const pattern = matchScriptPathExact(lodash.escapeRegExp(path));
for (const k of this.runningScriptMap.keys()) {
if (pattern.test(k)) {
return true;
}
}
@@ -216,7 +217,12 @@ export abstract class BaseServer implements IServer {
* be run.
*/
runScript(script: RunningScript): void {
this.runningScripts.push(script);
let byPid = this.runningScriptMap.get(script.scriptKey);
if (!byPid) {
byPid = new Map();
this.runningScriptMap.set(script.scriptKey, byPid);
}
byPid.set(script.pid, script);
}
setMaxRam(ram: number): void {
@@ -279,4 +285,36 @@ export abstract class BaseServer implements IServer {
if (hasTextExtension(path)) return this.writeToTextFile(path, content);
return this.writeToScriptFile(path, content);
}
// Serialize the current object to a JSON save state
// Called by subclasses, not stringify.
toJSONBase(ctorName: string, keys: readonly (keyof this)[]): IReviverValue {
// RunningScripts are stored as a simple array, both for backward compatibility,
// compactness, and ease of filtering them here.
const result = Generic_toJSON(ctorName, this, keys);
const rsArray: RunningScript[] = [];
for (const byPid of this.runningScriptMap.values()) {
for (const rs of byPid.values()) {
if (!rs.temporary) {
rsArray.push(rs);
}
}
}
result.data.runningScripts = rsArray;
return result;
}
// Initializes a Server Object from a JSON save state
// Called by subclasses, not Reviver.
static fromJSONBase<T extends BaseServer>(value: IReviverValue, ctor: new () => T, keys: readonly (keyof T)[]): T {
const result = Generic_fromJSON(ctor, value.data, keys);
result.savedScripts = value.data.runningScripts;
return result;
}
// Customize a prune list for a subclass.
static getIncludedKeys<T extends BaseServer>(ctor: new () => T): readonly (keyof T)[] {
return getKeyList(ctor, { removedKeys: ["runningScriptMap", "savedScripts", "ramUsed"] });
}
}
+4 -3
View File
@@ -5,7 +5,7 @@ import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { createRandomString } from "../utils/helpers/createRandomString";
import { createRandomIp } from "../utils/IPAddress";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { IPAddress, ServerName } from "../Types/strings";
export interface IConstructorParams {
@@ -147,13 +147,14 @@ export class Server extends BaseServer {
/** Serialize the current object to a JSON save state */
toJSON(): IReviverValue {
return Generic_toJSON("Server", this);
return this.toJSONBase("Server", includedKeys);
}
// Initializes a Server Object from a JSON save state
static fromJSON(value: IReviverValue): Server {
return Generic_fromJSON(Server, value.data);
return BaseServer.fromJSONBase(value, Server, includedKeys);
}
}
const includedKeys = BaseServer.getIncludedKeys(Server);
constructorsForReviver.Server = Server;
+3 -1
View File
@@ -252,7 +252,6 @@ export function prestigeHomeComputer(homeComp: Server): void {
const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume);
homeComp.programs.length = 0; //Remove programs
homeComp.runningScripts = [];
homeComp.serversOnNetwork = [];
homeComp.isConnectedTo = true;
homeComp.ramUsed = 0;
@@ -263,6 +262,9 @@ export function prestigeHomeComputer(homeComp: Server): void {
homeComp.messages.length = 0; //Remove .lit and .msg files
homeComp.messages.push(LiteratureName.HackersStartingHandbook);
if (homeComp.runningScriptMap.size !== 0) {
throw new Error("All programs weren't already killed!");
}
}
// Returns the i-th server on the specified server's network
+9 -4
View File
@@ -11,6 +11,7 @@ import { Player } from "@player";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { isPowerOfTwo } from "../utils/helpers/isPowerOfTwo";
import { WorkerScript } from "../Netscript/WorkerScript";
import { workerScripts } from "../Netscript/WorkerScripts";
// Returns the cost of purchasing a server with the given RAM
@@ -72,12 +73,16 @@ export const renamePurchasedServer = (hostname: string, newName: string): void =
const home = Player.getHomeComputer();
home.serversOnNetwork = replace(home.serversOnNetwork, hostname, newName);
server.serversOnNetwork = replace(server.serversOnNetwork, hostname, newName);
server.runningScripts.forEach((r) => (r.server = newName));
for (const byPid of server.runningScriptMap.values()) {
for (const r of byPid.values()) {
r.server = newName;
// Lookup can't fail.
const ws = workerScripts.get(r.pid) as WorkerScript;
ws.hostname = newName;
}
}
server.scripts.forEach((r) => (r.server = newName));
server.hostname = newName;
workerScripts.forEach((w) => {
if (w.hostname === hostname) w.hostname = newName;
});
renameServer(hostname, newName);
};