Skip to content

Commit

Permalink
[MIRROR] Controller Overview UI (#2076)
Browse files Browse the repository at this point in the history
* Controller Overview UI (#82739)

* Controller Overview UI

---------

Co-authored-by: Zephyr <[email protected]>
  • Loading branch information
2 people authored and StealsThePRs committed Apr 19, 2024
1 parent cd8ed59 commit 51dfb7c
Show file tree
Hide file tree
Showing 2 changed files with 321 additions and 3 deletions.
79 changes: 76 additions & 3 deletions code/controllers/master.dm
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ GLOBAL_REAL(Master, /datum/controller/master)
///used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits
var/static/current_ticklimit = TICK_LIMIT_RUNNING

/// Whether the Overview UI will update as fast as possible for viewers.
var/overview_fast_update = FALSE

/datum/controller/master/New()
if(!config)
config = new
Expand Down Expand Up @@ -135,6 +138,78 @@ GLOBAL_REAL(Master, /datum/controller/master)
ss.Shutdown()
log_world("Shutdown complete")

ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "View the current states of the Subsystem Controllers.", ADMIN_CATEGORY_SERVER)
Master.ui_interact(user.mob)

/datum/controller/master/ui_status(mob/user, datum/ui_state/state)
if(!user.client?.holder?.check_for_rights(R_SERVER|R_DEBUG))
return UI_CLOSE
return UI_INTERACTIVE

/datum/controller/master/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(isnull(ui))
ui = new /datum/tgui(user, src, "ControllerOverview")
ui.open()

/datum/controller/master/ui_data(mob/user)
var/list/data = list()

var/list/subsystem_data = list()
for(var/datum/controller/subsystem/subsystem as anything in subsystems)
subsystem_data += list(list(
"name" = subsystem.name,
"ref" = REF(subsystem),
"init_order" = subsystem.init_order,
"last_fire" = subsystem.last_fire,
"next_fire" = subsystem.next_fire,
"can_fire" = subsystem.can_fire,
"doesnt_fire" = !!(subsystem.flags & SS_NO_FIRE),
"cost_ms" = subsystem.cost,
"tick_usage" = subsystem.tick_usage,
"tick_overrun" = subsystem.tick_overrun,
"initialized" = subsystem.initialized,
"initialization_failure_message" = subsystem.initialization_failure_message,
))
data["subsystems"] = subsystem_data
data["world_time"] = world.time
data["map_cpu"] = world.map_cpu
data["fast_update"] = overview_fast_update

return data

/datum/controller/master/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
if(..())
return TRUE

switch(action)
if("toggle_fast_update")
overview_fast_update = !overview_fast_update
return TRUE

if("view_variables")
var/datum/controller/subsystem/subsystem = locate(params["ref"]) in subsystems
if(isnull(subsystem))
to_chat(ui.user, span_warning("Failed to locate subsystem."))
return
SSadmin_verbs.dynamic_invoke_verb(ui.user, /datum/admin_verb/debug_variables, subsystem)
return TRUE

/datum/controller/master/proc/check_and_perform_fast_update()
PRIVATE_PROC(TRUE)
set waitfor = FALSE


if(!overview_fast_update)
return

var/static/already_updating = FALSE
if(already_updating)
return
already_updating = TRUE
SStgui.update_uis(src)
already_updating = FALSE

// Returns 1 if we created a new mc, 0 if we couldn't due to a recent restart,
// -1 if we encountered a runtime trying to recreate it
/proc/Recreate_MC()
Expand Down Expand Up @@ -582,11 +657,9 @@ GLOBAL_REAL(Master, /datum/controller/master)
if (processing * sleep_delta <= world.tick_lag)
current_ticklimit -= (TICK_LIMIT_RUNNING * 0.25) //reserve the tail 1/4 of the next tick for the mc if we plan on running next tick

check_and_perform_fast_update()
sleep(world.tick_lag * (processing * sleep_delta))




// This is what decides if something should run.
/datum/controller/master/proc/CheckQueue(list/subsystemstocheck)
. = 0 //so the mc knows if we runtimed
Expand Down
245 changes: 245 additions & 0 deletions tgui/packages/tgui/interfaces/ControllerOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { BooleanLike } from 'common/react';
import { createSearch } from 'common/string';
import { useMemo, useState } from 'react';

import { useBackend } from '../backend';
import {
Button,
Collapsible,
Dropdown,
Input,
LabeledList,
Section,
Stack,
} from '../components';
import { Window } from '../layouts';

type SubsystemData = {
name: string;
ref: string;
init_order: number;
last_fire: number;
next_fire: number;
can_fire: BooleanLike;
doesnt_fire: BooleanLike;
cost_ms: number;
tick_usage: number;
tick_overrun: number;
initialized: BooleanLike;
initialization_failure_message: string | undefined;
};

type ControllerData = {
world_time: number;
fast_update: BooleanLike;
map_cpu: number;
subsystems: SubsystemData[];
};

const SubsystemView = (props: { data: SubsystemData }) => {
const { act } = useBackend();
const { data } = props;
const {
name,
ref,
init_order,
last_fire,
next_fire,
can_fire,
doesnt_fire,
cost_ms,
tick_usage,
tick_overrun,
initialized,
initialization_failure_message,
} = data;

let icon = 'play';
if (!initialized) {
icon = 'circle-exclamation';
} else if (doesnt_fire) {
icon = 'check';
} else if (!can_fire) {
icon = 'pause';
}

return (
<Collapsible
title={name}
key={ref}
icon={icon}
buttons={
<Button
icon="wrench"
tooltip="View Variables"
onClick={() => {
act('view_variables', { ref: ref });
}}
/>
}
>
<LabeledList>
<LabeledList.Item label="Init Order">{init_order}</LabeledList.Item>
<LabeledList.Item label="Last Fire">{last_fire}</LabeledList.Item>
<LabeledList.Item label="Next Fire">{next_fire}</LabeledList.Item>
<LabeledList.Item label="Cost">{cost_ms}ms</LabeledList.Item>
<LabeledList.Item label="Tick Usage">
{(tick_usage * 0.01).toFixed(2)}%
</LabeledList.Item>
<LabeledList.Item label="Tick Overrun">
{(tick_overrun * 0.01).toFixed(2)}%
</LabeledList.Item>
{initialization_failure_message ? (
<LabeledList.Item color="bad">
{initialization_failure_message}
</LabeledList.Item>
) : undefined}
</LabeledList>
</Collapsible>
);
};

enum SubsystemSortBy {
INIT_ORDER = 'Init Order',
NAME = 'Name',
LAST_FIRE = 'Last Fire',
NEXT_FIRE = 'Next Fire',
TICK_USAGE = 'Tick Usage',
TICK_OVERRUN = 'Tick Overrun',
COST = 'Cost',
}

const sortSubsystemBy = (
subsystems: SubsystemData[],
sortBy: SubsystemSortBy,
asending: boolean = true,
) => {
let sorted = subsystems.sort((left, right) => {
switch (sortBy) {
case SubsystemSortBy.INIT_ORDER:
return left.init_order - right.init_order;
case SubsystemSortBy.NAME:
return left.name.localeCompare(right.name);
case SubsystemSortBy.LAST_FIRE:
return left.last_fire - right.last_fire;
case SubsystemSortBy.NEXT_FIRE:
return left.next_fire - right.next_fire;
case SubsystemSortBy.TICK_USAGE:
return left.tick_usage - right.tick_usage;
case SubsystemSortBy.TICK_OVERRUN:
return left.tick_overrun - right.tick_overrun;
case SubsystemSortBy.COST:
return left.cost_ms - right.cost_ms;
}
});
if (!asending) {
sorted.reverse();
}
return sorted;
};

export const ControllerOverview = () => {
const { act, data } = useBackend<ControllerData>();
const { world_time, map_cpu, subsystems, fast_update } = data;

const [filterName, setFilterName] = useState('');
const [sortBy, setSortBy] = useState(SubsystemSortBy.NAME);
const [sortAscending, setSortAscending] = useState<boolean>(true);

let filteredSubsystems = useMemo(() => {
if (!filterName) {
return subsystems;
}

return subsystems.filter(() =>
createSearch(filterName, (subsystem: SubsystemData) => subsystem.name),
);
}, [filterName, subsystems]);

let sortedSubsystems = useMemo(() => {
return sortSubsystemBy(filteredSubsystems, sortBy, sortAscending);
}, [sortBy, sortAscending, filteredSubsystems]);

const overallUsage = subsystems.reduce(
(acc, subsystem) => acc + subsystem.tick_usage,
0,
);
const overallOverrun = subsystems.reduce(
(acc, subsystem) => acc + subsystem.tick_overrun,
0,
);

return (
<Window>
<Window.Content>
<Section title="Master Overview">
<Stack vertical>
<Stack.Item>World Time: {world_time}</Stack.Item>
<Stack.Item>Map CPU: {map_cpu.toFixed(2)}%</Stack.Item>
<Stack.Item>
Overall Usage: {(overallUsage * 0.01).toFixed(2)}%
</Stack.Item>
<Stack.Item>
Overall Overrun: {(overallOverrun * 0.01).toFixed(2)}%
</Stack.Item>
</Stack>
</Section>
<Section
title="Filtering/Sorting"
buttons={
<Button
tooltip="Fast Update"
icon="fast-forward"
color={fast_update ? 'good' : 'bad'}
onClick={() => {
act('toggle_fast_update');
}}
/>
}
>
<Input
placeholder="Filter by name"
value={filterName}
onChange={(e, value) => setFilterName(value)}
/>
<Button
icon="trash"
tooltip="Reset filter"
onClick={() => setFilterName('')}
disabled={filterName === undefined}
/>
<Dropdown
options={Object.values(SubsystemSortBy)}
selected={sortBy}
onSelected={(value) => setSortBy(value as SubsystemSortBy)}
/>
<Stack>
<Stack.Item>
<Button
selected={sortAscending}
onClick={() => setSortAscending(true)}
>
Ascending
</Button>
</Stack.Item>
<Stack.Item>
<Button
selected={!sortAscending}
onClick={() => setSortAscending(false)}
>
Descending
</Button>
</Stack.Item>
</Stack>
</Section>
<Section title="Subsystem Overview" scrollable>
<Stack vertical>
{sortedSubsystems.map((subsystem) => (
<SubsystemView key={subsystem.ref} data={subsystem} />
))}
</Stack>
</Section>
</Window.Content>
</Window>
);
};

0 comments on commit 51dfb7c

Please sign in to comment.