Skip to content

Add Apple Silicon GPU monitoring and publish as System Vitals - #1

Merged
sachinsachdeva merged 9 commits into
masterfrom
feature/apple-silicon-gpu
Aug 8, 2026
Merged

Add Apple Silicon GPU monitoring and publish as System Vitals#1
sachinsachdeva merged 9 commits into
masterfrom
feature/apple-silicon-gpu

Conversation

@sachinsachdeva

Copy link
Copy Markdown
Owner

Adds GPU monitoring on macOS and prepares the fork to be published under its own marketplace identity.

GPU monitoring

systeminformation cannot supply GPU utilization on macOS — si.graphics() shells out to system_profiler, costs ~230ms, and has no utilization field. Statistics instead come from the IOKit registry via ioreg -r -d 1 -w 0 -c IOAccelerator, which takes ~20ms and needs no elevated privileges, unlike powermetrics.

Two independently toggleable resources: GPU utilization and GPU memory. Availability is gated on whether a machine actually reports usable statistics rather than on architecture, so this covers Intel Macs too and stays hidden everywhere else.

GPU memory reads as in use / allocated, not used-of-VRAM — Apple Silicon has unified memory with no fixed GPU partition, so the denominator is what the driver has currently claimed.

The sampler caches for 100ms so one tick costs a single ioreg call across both resources, and resolves to null rather than rejecting on failure. That last part matters: ResMon.update() gathers resources with Promise.all and has no error handling, so one rejection would break the setTimeout chain and freeze the entire status bar.

Packaging fix

systeminformation was listed in both dependencies and devDependencies, and the lockfile recorded it "dev": true. vsce pruned it and produced a VSIX with no node_modules at all, so an installed build died on activation at require('systeminformation'). Running from source with F5 was unaffected, which is why it went unnoticed.

Rebrand

Published as sachinsachdeva.system-vitals, version reset to 1.0.0. New icon, since the previous one was the upstream author's artwork. LICENSE.md records that upstream declares no license, so redistribution terms are unresolved, and that only the changes made here are ours to license.

Settings move from resmon.* to systemvitals.* so the two extensions stay independent when both are installed. Existing settings do not migrate.

Activation moves from * to onStartupFinished, which requires VS Code 1.53. That forced @types/vscode in place of the deprecated vscode module, and TypeScript 2.6 to 5.9. One line of source changed: a meaningless async on an abstract declaration, which TypeScript 4+ rejects.

Bug fixes

  • Settings were frozen at activation. Each Resource held the WorkspaceConfiguration snapshot it was constructed with, so show.* and unit changes needed a window reload. Now pushed to each resource per tick.
  • That exposed a second issue: _maxWidth only grows, so a setting that shortens a display left the bar padded at the old width. A configuration listener now resets it.
  • Memory read 'memunit' instead of the contributed 'mem.unit', so it always rendered GB.
  • convertBytesToLargestUnit labelled units from the enum, yielding "G" and "None" instead of "GB" and "B".
  • The base isShown() hardcoded false instead of using _isShownByDefault, making that argument dead.
  • Removed the unfinished Network resource, which logged every interface to the console on activation and returned "". With isShown() honouring _isShownByDefault it would have started rendering an empty column.

Verification

  • Parser matched grep exactly on identical captured ioreg text across three snapshots; empty, garbage, truncated and missing-key inputs all return null without throwing; the "In use system memory (driver)" sibling key is not mistaken for the real one.
  • Sampler: 4 concurrent reads produce 1 spawn, cached reads produce 0, ioreg unreachable resolves null without rejecting, and non-darwin returns null with zero spawns.
  • End-to-end against a stubbed vscode API across six configurations — defaults, GPU memory on, gpu.unit: MB, precision: 0, both toggles off, and forced non-macOS.
  • Live settings changes verified mid-session with no reload and no re-activation.
  • vsce packages with zero warnings and no override flags, where it previously needed --allow-star-activation and --allow-missing-repository.

Not yet verified: installing the VSIX and exercising the onStartupFinished path inside a real editor.

sachinsachdeva and others added 7 commits August 8, 2026 23:22
Adds GPU utilization and GPU memory as two independently toggleable
status bar resources.

systeminformation cannot supply this: si.graphics() shells out to
system_profiler, costs ~230ms, and reports no utilization field at all.
Instead, statistics are read from the IOKit registry with
`ioreg -r -d 1 -w 0 -c IOAccelerator`, which takes ~20ms and needs no
elevated privileges, unlike powermetrics.

Availability is decided by whether a machine actually reports usable
statistics rather than by checking the architecture, so this also covers
Intel Macs and stays hidden anywhere the data is missing.

The sampler caches for 100ms so that a single update tick costs one ioreg
invocation across both resources, and resolves to null rather than
rejecting on any failure. That matters because ResMon.update() collects
resources with Promise.all and has no error handling, so one rejection
would break the setTimeout chain and freeze the whole status bar.

GPU memory is reported as in-use over allocated, not used out of VRAM:
Apple Silicon uses unified memory with no fixed GPU partition, so the
second figure is what the driver has currently claimed from system
memory rather than a fixed capacity.

Also documents the existing behaviour that resources capture their
configuration once at activation, so show.* and unit settings need a
window reload to take effect.

New settings: resmon.show.gpu, resmon.show.gpumem, resmon.gpu.unit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
systeminformation was listed in both dependencies and devDependencies,
and the lockfile recorded it with "dev": true. As a result vsce pruned it
as a dev dependency and produced a VSIX containing no node_modules at
all, so an installed build failed at activation on
require('systeminformation'). Running from source with F5 was unaffected,
which is why this went unnoticed.

Drops the duplicate devDependencies entry and clears the stale dev flag
in the lockfile. Verified with `npm ci --omit=dev`, which now resolves
the dependency, and by repackaging: the VSIX went from 13 files with no
node_modules to 34 files including systeminformation.

Also excludes .claude and .github from the package, neither of which
belongs in a published extension.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prepares the fork for publishing under its own marketplace identity
rather than the upstream author's.

Identity: publisher sachinsachdeva, id system-vitals, version reset to
1.0.0 for a fresh listing. Repository, bugs and homepage now point at
this fork. The icon is replaced, as the previous one was the upstream
author's artwork.

Adds LICENSE.md recording that upstream declares no license, so the
redistribution terms for this project are unresolved, and that only the
changes made in this fork are ours to license. This documents the
situation rather than asserting rights over code we do not own.

Switches activation from "*" to onStartupFinished so the extension no
longer loads during editor startup. That API needs VS Code 1.53, which
forced the rest: @types/vscode 1.53 replaces the deprecated vscode
module and its postinstall hook, and since those typings use `unknown`,
TypeScript moves from 2.6 to 5.9. The only source change that required
was dropping a meaningless `async` from an abstract method declaration,
which TypeScript 4+ rejects.

vsce now packages with no warnings and no override flags, where it
previously needed --allow-star-activation and --allow-missing-repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With System Vitals published as its own extension, sharing the resmon.*
configuration namespace with the upstream project meant that installing
both would give you two status bars driven by one set of keys, with no
way to configure them independently.

Renames all 17 contributed settings and both getConfiguration() call
sites. Resource subclasses are unaffected, as they address keys relative
to the namespace.

The repository URL keeps the resmon name, since the GitHub repository is
still called that; only the configuration namespace moves.

Settings do not migrate: anyone coming from resmon has to set their
preferences again, which the changelog now states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings now apply live. Each Resource held the WorkspaceConfiguration it
was handed at construction, and because that object is an immutable
snapshot, the refreshed one ResMon fetched every tick never reached it.
Every show.* and unit setting was therefore fixed at activation and
needed a window reload. Adds setConfig(), called on each resource per
tick.

That exposed a second problem: _maxWidth only ever grows, so a setting
that shortens a display, such as switching memory from B to GB, would
leave the status bar padded at the old width for the rest of the session.
A configuration listener now resets the high water mark when settings
actually change.

Memory ignored its unit setting: it read the key 'memunit' while the
contributed key is 'mem.unit', so it always rendered GB.

convertBytesToLargestUnit labelled its output from the Units enum, which
yields "G" and "None" rather than "GB" and "B". Adds an explicit label
lookup. Its unused precision parameter is gone; it already deferred to
getPrecision().

The base isShown() hardcoded false instead of using _isShownByDefault,
making that constructor argument dead. Now used, and the two values that
disagreed with the manifest (cputemp, battery) are aligned with it. This
is a no-op for contributed keys, since VS Code returns the contributed
default, but it removes a trap for any key added later.

Removes the unfinished Network resource. It logged every interface to the
console on activation and its getDisplay() returned "". With isShown()
honouring _isShownByDefault it would have started rendering an empty
column, since show.net is not a contributed key and it was constructed
with a default of true. Recoverable from 8720281 if picked back up.

Corrects the show.precision description, which was copied from
updatefrequencyms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two screenshots carried over from upstream were the original author's
captures of the original extension: a Windows status bar showing a C:
drive, with no GPU metrics. Same provenance question as the icon, and no
longer an accurate picture of what this extension displays.

Replaces them with a capture of System Vitals running on Apple Silicon,
showing the default set of CPU usage, CPU frequency, GPU utilization and
memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI could not have passed. It targeted Node 10 and 12, both long EOL and
below the 14.17 that TypeScript 5 requires, and invoked a bare `tsc`,
which is not on PATH in a run step. It also used checkout@v2 and
setup-node@v1.

Rebuilds it on Node 20 and 22 with current actions, and adds lint, test
and package steps. Packaging in CI is the point: the systeminformation
bug shipped a broken extension that compiled and linted cleanly, and only
inspecting the built artifact catches that class of problem, so the
workflow now asserts the runtime dependency is present in the VSIX and
uploads it for download from the pull request.

Adds unit tests over the logic that can run outside the editor, using the
Node test runner so nothing new is added to the dependency tree. They
cover the ioreg parser, including the "In use system memory (driver)"
sibling key that a looser pattern would mistake for the real value, and
its behaviour on empty, malformed and truncated input; the sampler's
guarantee that it resolves rather than rejecting, since a rejection would
freeze the status bar; and that every unit and disk format offered in the
manifest has a mapping behind it.

Tests live in src/test and compile to out/test, which .vscodeignore
already excludes, so they do not ship. Verified: the packaged VSIX
contains no test files.

Drops @types/mocha, which was unused, and whose global describe/it
declarations would have shadowed the test runner's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sachinsachdeva and others added 2 commits August 9, 2026 02:05
checkout, setup-node and upload-artifact at v4 target the Node 20 action
runtime, which GitHub has deprecated and is already force-running on
Node 24. v5 targets Node 24 directly, which clears the warning on every
run before it becomes a failure.

The build matrix is unaffected: it selects the Node used to compile and
test, not the runtime the actions themselves execute on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Node 20 is in maintenance and its action runtime is already deprecated on
GitHub runners. Testing against the current LTS and the current release
matches where users and the runners actually are.

The VSIX upload stays pinned to the 22.x job, which is still in the
matrix, so exactly one artifact is produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sachinsachdeva
sachinsachdeva merged commit 99c118f into master Aug 8, 2026
2 checks passed
@sachinsachdeva
sachinsachdeva deleted the feature/apple-silicon-gpu branch August 8, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant