<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ilia Alshanetsky</title>
    <description>The latest articles on Forem by Ilia Alshanetsky (@iliaa).</description>
    <link>https://forem.com/iliaa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866511%2Ff435f62a-66cb-4c1a-bdf4-aa79870ddd9a.png</url>
      <title>Forem: Ilia Alshanetsky</title>
      <link>https://forem.com/iliaa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/iliaa"/>
    <language>en</language>
    <item>
      <title>Driving TradingView Desktop From an AI Agent</title>
      <dc:creator>Ilia Alshanetsky</dc:creator>
      <pubDate>Wed, 29 Apr 2026 22:11:00 +0000</pubDate>
      <link>https://forem.com/iliaa/driving-tradingview-desktop-from-an-ai-agent-7d</link>
      <guid>https://forem.com/iliaa/driving-tradingview-desktop-from-an-ai-agent-7d</guid>
      <description>&lt;p&gt;Anyone who's tried AI-assisted trading research has hit the same wall.&lt;/p&gt;

&lt;p&gt;The agent has no native access to your charts. You end up copy-pasting symbols, indicator values, screenshots, and Pine Script back and forth between TradingView and Claude or Cursor. The tools that try to fix this fall into two camps: route market data through a third-party API (added latency, added cost, their interpretation of your bars), or pollute your TradingView chart with helper indicators just so an agent can read them back.&lt;/p&gt;

&lt;p&gt;There's a third path that's more obvious in retrospect: drive your &lt;em&gt;local&lt;/em&gt; TradingView Desktop through the Chrome DevTools Protocol that the app already exposes on port 9222 when you launch it with &lt;code&gt;--remote-debugging-port=9222&lt;/code&gt;. The agent talks to the same TradingView you already use, reads what your chart actually shows, executes Pine Script through TradingView's own runtime. Same data, same indicators, same features, just with an agent in the loop.&lt;/p&gt;

&lt;p&gt;A project called &lt;code&gt;tradesdontlie/tradingview-mcp&lt;/code&gt; started down this path. Then it stalled with TV Desktop 3.1.0 incompatibilities and a handful of bugs, and the maintainer went quiet on PRs. I forked it, and now 71 commits later just tagged &lt;code&gt;1.0.0&lt;/code&gt; of the fork. This post covers what I added and what the tool doesn't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in 1.0.0
&lt;/h2&gt;

&lt;p&gt;The release is at &lt;a href="https://github.com/iliaal/tradingview-mcp/releases/tag/1.0.0" rel="noopener noreferrer"&gt;https://github.com/iliaal/tradingview-mcp/releases/tag/1.0.0&lt;/a&gt; with the per-tool changelog. The high-level numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;78 → 96 tools (18 new MCP tools)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;tv&lt;/code&gt; CLI (30 commands, 66 subcommands) mirroring the MCP surface&lt;/li&gt;
&lt;li&gt;TV Desktop 3.1.0 compatibility across the whole surface&lt;/li&gt;
&lt;li&gt;338 offline tests (pattern detection, multi-timeframe, replay, CLI routing)&lt;/li&gt;
&lt;li&gt;Removed &lt;code&gt;ui_evaluate&lt;/code&gt; from the upstream surface (it accepted arbitrary JavaScript in the user's authenticated TV session)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pine Script Lifecycle
&lt;/h2&gt;

&lt;p&gt;The original project let an agent inspect Pine Script, but only barely. Editing, saving, switching, and version history all required the user to click through TradingView's UI. The 1.0.0 fork closes that gap with eight new tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pine_save_as&lt;/code&gt; and &lt;code&gt;pine_rename&lt;/code&gt; for naming and copying scripts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pine_version_history&lt;/code&gt;, &lt;code&gt;pine_delete&lt;/code&gt;, &lt;code&gt;pine_switch_script&lt;/code&gt; for managing the Pine library&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pine_smart_compile&lt;/code&gt; (auto-detects whether to add or update an indicator on the chart; returns &lt;code&gt;elapsed_ms&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pine_analyze&lt;/code&gt; (offline static analysis; catches typos, unused vars, deprecated patterns before compile)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pine_check&lt;/code&gt; (server-side compile without putting the script on a chart; useful for CI-style validation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;pine_check&lt;/code&gt; + &lt;code&gt;pine_analyze&lt;/code&gt; pair is the unlock for AI-assisted Pine debugging. The agent writes a draft, runs &lt;code&gt;pine_analyze&lt;/code&gt; for cheap structural checks, then &lt;code&gt;pine_check&lt;/code&gt; for the real compile, reads errors, fixes, repeats. The user never has to add an unfinished script to the chart just to see whether it compiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pine Drawing Readers
&lt;/h2&gt;

&lt;p&gt;This is the part I keep using most. Five new tools let the agent read what a Pine indicator actually drew, after it's running on the chart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data_get_pine_lines&lt;/code&gt;: horizontal price levels (support, resistance, trend lines)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data_get_pine_labels&lt;/code&gt;: text annotations (signal labels, divergence callouts)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data_get_pine_tables&lt;/code&gt;: table cells (stats panels, multi-symbol scanners)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data_get_pine_boxes&lt;/code&gt;: price zones (supply, demand, order blocks)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data_get_pine_shapes&lt;/code&gt;: &lt;code&gt;plotshape&lt;/code&gt; and &lt;code&gt;plotchar&lt;/code&gt; markers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default behavior deduplicates and caps output (50 labels by default). Override with &lt;code&gt;verbose=true&lt;/code&gt; or &lt;code&gt;max_labels=N&lt;/code&gt; for cases where the agent needs the raw set.&lt;/p&gt;

&lt;p&gt;Why this matters: agents writing Pine Script could previously check whether the script &lt;em&gt;compiled&lt;/em&gt;, but couldn't verify whether the logic was &lt;em&gt;correct&lt;/em&gt;. Now the agent can write an indicator, compile it, and read the labels back to confirm the right bars were tagged. Without this, AI-assisted Pine work stops at "syntax is fine, hope you wrote what you meant."&lt;/p&gt;

&lt;p&gt;This also replaces the workflow that an agent would otherwise fall back to: screenshot the chart, run OCR over the image, hope the agent pieces together what the indicator drew. That path is slow (a screenshot round-trip plus OCR latency on every read), unreliable (overlapping labels and thin lines confuse the OCR), and expensive (chart images in the agent's context burn thousands of tokens before any reasoning happens). The structured readers skip all three problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Pane and Tab Navigation
&lt;/h2&gt;

&lt;p&gt;Pine indicators rarely live alone. A real chart layout might have three panes (price + RSI + volume) and four tabs, one per ticker. The original project assumed a single chart context. The fork adds eleven tools that make the layout addressable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pane_list&lt;/code&gt;, &lt;code&gt;pane_focus&lt;/code&gt;, &lt;code&gt;pane_set_layout&lt;/code&gt;, &lt;code&gt;pane_set_symbol&lt;/code&gt;, &lt;code&gt;pane_set_timeframe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pane_read_batch&lt;/code&gt; (single call that reads across all visible panes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tab_list&lt;/code&gt;, &lt;code&gt;tab_new&lt;/code&gt;, &lt;code&gt;tab_close&lt;/code&gt;, &lt;code&gt;tab_switch&lt;/code&gt;, &lt;code&gt;tab_switch_by_name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pane_read_batch&lt;/code&gt; was the practical motivator: agents that need to compare indicator values across panes used to need 5+ round-trips. The batch reader collapses that to one CDP call, which matters when the agent is iterating quickly.&lt;/p&gt;

&lt;p&gt;A non-obvious detail: pane focus needs a 300ms wait after &lt;code&gt;pane.focus()&lt;/code&gt; for &lt;code&gt;_activeChartWidgetWV&lt;/code&gt; to update, required since TV 3.1.0. The fork bakes this in so users don't have to discover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Timeframe and Pattern Detection
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;data_get_multi_timeframe&lt;/code&gt; reads indicator values across a list of timeframes (W → D → 4H → 1H → 15m alignment) in one call. It saves and restores the original timeframe, so the agent gets multi-timeframe context without disrupting the user's view.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data_detect_candlestick_patterns&lt;/code&gt; runs 17 classic patterns over raw OHLC bars: doji, hammer, hanging_man, inverted_hammer, shooting_star, marubozu, spinning_top, bullish_engulfing, bearish_engulfing, bullish_harami, bearish_harami, piercing_line, dark_cloud_cover, morning_star, evening_star, three_white_soldiers, three_black_crows. Each match returns the pattern name, a direction (bullish/bearish/neutral), and a strength score.&lt;/p&gt;

&lt;p&gt;The point of doing pattern detection natively, instead of via a Pine indicator overlay, is chart hygiene. Pattern detection informs the agent; the user never wants to see it plotted on the chart. Running it over OHLC bars directly means no Pine indicator gets added to the chart, no scripts get pushed to TV's Save dialog, and the user's chart stays clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;tv&lt;/code&gt; CLI
&lt;/h2&gt;

&lt;p&gt;The MCP surface is meant for AI agents. But the same operations are useful from a shell, and shipping both interfaces from the same code path (instead of building a separate CLI program) keeps them in sync.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tv&lt;/code&gt; ships 30 commands with 66 subcommands. A few examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tv quote get AAPL                          &lt;span class="c"&gt;# current quote&lt;/span&gt;
tv data ohlcv AAPL &lt;span class="nt"&gt;--tf&lt;/span&gt; 1H &lt;span class="nt"&gt;--bars&lt;/span&gt; 200      &lt;span class="c"&gt;# OHLCV bars&lt;/span&gt;
tv pine check ./my_indicator.pine          &lt;span class="c"&gt;# offline compile&lt;/span&gt;
tv pine smart_compile ./my_indicator.pine  &lt;span class="c"&gt;# add or update on chart&lt;/span&gt;
tv hotlist volume_gainers &lt;span class="nt"&gt;--country&lt;/span&gt; US     &lt;span class="c"&gt;# scanner&lt;/span&gt;
tv stream all                              &lt;span class="c"&gt;# poll-and-diff JSONL output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tv stream&lt;/code&gt; polls the chart and emits JSONL whenever something changes: a new bar, an indicator value update, a new label drawn. Useful for piping into tail-and-watch workflows or for sanity-checking a strategy as it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Platform Launch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tv_launch&lt;/code&gt; auto-detects native macOS, Linux, Windows, and Windows MSIX (Microsoft Store) installs of TradingView Desktop, resolves Windows paths correctly when invoked from WSL2, and handles the case where a binary refuses &lt;code&gt;--remote-debugging-port&lt;/code&gt; from a direct spawn. macOS Electron 38 is the worst offender there; the fix is to fall back to &lt;code&gt;open -a&lt;/code&gt; with the port flag in &lt;code&gt;--args&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The motivation for sinking this much code into launch behavior is operational: in a CI-like setup or a containerized agent loop, you want &lt;code&gt;tv_ensure&lt;/code&gt; to be a no-op when TV is already running and a clean launch when it isn't, on whichever OS the agent happens to be on. The original project assumed the user had TradingView running and never had to think about how it got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quiet Architectural Change
&lt;/h2&gt;

&lt;p&gt;One refactor: per-call &lt;code&gt;_deps&lt;/code&gt; dependency injection across ten core modules (&lt;code&gt;chart&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;drawing&lt;/code&gt;, &lt;code&gt;pane&lt;/code&gt;, &lt;code&gt;pine&lt;/code&gt;, &lt;code&gt;replay&lt;/code&gt;, &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;watchlist&lt;/code&gt;, &lt;code&gt;alerts&lt;/code&gt;, &lt;code&gt;capture&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Before: each module imported &lt;code&gt;connection.js&lt;/code&gt; directly. State lived in module globals. Tests had to monkey-patch &lt;code&gt;connection.js&lt;/code&gt; to swap CDP behavior.&lt;/p&gt;

&lt;p&gt;After: each function accepts a &lt;code&gt;_deps&lt;/code&gt; parameter, so the test harness passes in a mock CDP wrapper without touching globals. The &lt;code&gt;installCdpMocks&lt;/code&gt; and &lt;code&gt;mockEvaluateFromTable&lt;/code&gt; helpers in &lt;code&gt;tests/helpers/mock-cdp.js&lt;/code&gt; give every test an isolated environment.&lt;/p&gt;

&lt;p&gt;Practical result: 338 offline tests run in seconds without needing TradingView Desktop open. The previous test surface was much smaller because anything beyond a unit test required the real app running. The DI refactor unlocked the test count, which is what makes the 3.1.0 compat fixes safe to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Removed: &lt;code&gt;ui_evaluate&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The upstream had a tool called &lt;code&gt;ui_evaluate&lt;/code&gt; that accepted arbitrary JavaScript and ran it in the authenticated TradingView session. Anyone with MCP access to the agent had effective full read/write access to the user's TradingView account state: watchlists, alerts, saved scripts, anything the session could see.&lt;/p&gt;

&lt;p&gt;Dropped it. There's no use case for arbitrary JavaScript that isn't better served by a specific tool with a narrow boundary. If you need to read or modify something the existing tools don't cover, that's a missing tool, not a reason to expose the JS runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Doesn't Do
&lt;/h2&gt;

&lt;p&gt;The agent talks to your &lt;em&gt;running&lt;/em&gt; TradingView Desktop via CDP. There's no API spoofing, no auth bypass, no headless TV. If you don't have TV Desktop installed, the tool can't help you.&lt;/p&gt;

&lt;p&gt;TradingView's free tier covers most of the lifecycle tools. Some features need paid tiers (multi-pane, replay mode, second-resolution data, more indicators per chart). The tools wrap whatever the local TV exposes; they don't unlock features you don't have.&lt;/p&gt;

&lt;p&gt;It doesn't bypass anything. It doesn't get you data TradingView doesn't already give you. The improvement is the agent surface, not the data access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/iliaal/tradingview-mcp
&lt;span class="nb"&gt;cd &lt;/span&gt;tradingview-mcp
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the server to &lt;code&gt;~/.claude/.mcp.json&lt;/code&gt; (or your MCP config). Launch TradingView with &lt;code&gt;--remote-debugging-port=9222&lt;/code&gt;. The README has the paste-into-Claude-Code one-liner that covers the agent-side wiring.&lt;/p&gt;

</description>
      <category>tradingview</category>
      <category>trading</category>
      <category>mcp</category>
      <category>showdev</category>
    </item>
    <item>
      <title>It's Alive! statgrab Returns After 20 Years</title>
      <dc:creator>Ilia Alshanetsky</dc:creator>
      <pubDate>Tue, 28 Apr 2026 21:15:29 +0000</pubDate>
      <link>https://forem.com/iliaa/its-alive-statgrab-returns-after-20-years-36e3</link>
      <guid>https://forem.com/iliaa/its-alive-statgrab-returns-after-20-years-36e3</guid>
      <description>&lt;p&gt;In 2005 I wrote a PHP binding for libstatgrab and pushed it to PECL. The extension took CPU, memory, disk I/O, network, process, and user statistics from a cross-platform C library and exposed them to PHP as plain functions. I moved on to other things, libstatgrab kept evolving, PHP went through three major versions, and the binding sat untouched. By 2020 you could not build it against PHP 7 without patches. By PHP 8 it was effectively gone.&lt;/p&gt;

&lt;p&gt;statgrab 2.0 brings it back. PHP 8.0 through 8.5, libstatgrab 0.92+, glibc Linux, musl, macOS, FreeBSD. The 2006 procedural API still works (&lt;code&gt;sg_cpu_percent_usage&lt;/code&gt;, &lt;code&gt;sg_memory_stats&lt;/code&gt;, &lt;code&gt;sg_diskio_stats&lt;/code&gt;), there is a modern OO surface (&lt;code&gt;Statgrab::cpu()&lt;/code&gt;, &lt;code&gt;Statgrab::memory()&lt;/code&gt;, &lt;code&gt;Statgrab::processes()&lt;/code&gt;), counters return as 64-bit &lt;code&gt;int&lt;/code&gt; instead of the 2006 stringified &lt;code&gt;%lld&lt;/code&gt;, and the BC bugs that were latent in the original (swapped page-stat keys, copy-pasted gid/egid fields, the flat &lt;code&gt;name_list&lt;/code&gt; for users) are fixed.&lt;/p&gt;

&lt;p&gt;A few things had to change to get there. One of them was upstreaming a memory leak fix to libstatgrab itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bring it back at all
&lt;/h2&gt;

&lt;p&gt;I do not pull old extensions forward by default. I learned why with &lt;a href="https://github.com/iliaal/lchash" rel="noopener noreferrer"&gt;&lt;code&gt;lchash&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;lchash is another extension I originally pushed to PECL in 2005: a string-keyed hash table for PHP, designed around the idea that PHP's array implementation, while general, carried ordering and bucket-reallocation overhead that pure key-value workloads did not need. Tighter memory footprint, faster lookup, simpler semantics ("first writer wins" like glibc &lt;code&gt;hsearch(ENTER)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I shipped a 1.0.0 modernization this week. Rebuilt the storage on top of &lt;a href="https://github.com/attractivechaos/klib" rel="noopener noreferrer"&gt;klib khash&lt;/a&gt;, got it green on PHP 7.4 through 8.5 NTS and ZTS, added a proper OO surface (&lt;code&gt;LcHash&lt;/code&gt; with &lt;code&gt;$obj[$key]&lt;/code&gt; dimension access), wrote a benchmark script and let it run.&lt;/p&gt;

&lt;p&gt;The numbers were not flattering. On a release build of PHP 8.4 NTS, glibc Linux x86_64, lchash takes 1.4x to 1.7x longer to insert and around 2x longer to look up than a native PHP array, at 10k, 100k, and 1M entries. That gap is structural. The PHP 7 array rewrite (Dmitry Stogov's packed-array work) and the 8.x JIT with inline caching produced a hash table the runtime treats as a first-class type, with opcode-level array-access specialization that no extension can match.&lt;/p&gt;

&lt;p&gt;The flip: lchash uses 40 to 80 percent of the memory PHP arrays do at the same entry count, because keys and values are stored as refcount-shared &lt;code&gt;zend_string&lt;/code&gt;s with no per-entry Bucket overhead. That makes the extension a real win for memory-tight workloads (a long-running CLI worker holding hundreds of thousands of small mappings), and it still has the legacy-compat and C-porting use cases it had in 2005. For general code, the answer is "just use a PHP array."&lt;/p&gt;

&lt;p&gt;I shipped lchash 1.0.0 anyway, with the benchmark table at the top of the README and the use cases honestly scoped. The lesson is not "do not revive things." It is: the revival has to be honest about what changed underneath. PHP arrays grew up. lchash now competes on memory, not speed, and the README says so before anyone has to find out.&lt;/p&gt;

&lt;p&gt;statgrab is not in that situation. PHP 8 arrays are not a substitute for cross-platform system stats. The choice today, for someone running PHP on a server who needs CPU, memory, or disk numbers, is still one of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shell out to &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;vmstat&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt; and parse output that drifts between OS versions. &lt;code&gt;fork&lt;/code&gt;+&lt;code&gt;exec&lt;/code&gt; overhead per call.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;/proc&lt;/code&gt; by hand. Linux-only, format keeps shifting between kernel releases, every file (&lt;code&gt;meminfo&lt;/code&gt;, &lt;code&gt;loadavg&lt;/code&gt;, &lt;code&gt;diskstats&lt;/code&gt;, &lt;code&gt;net/dev&lt;/code&gt;) has its own quirks.&lt;/li&gt;
&lt;li&gt;Run a separate stats daemon (collectd, telegraf, node_exporter) and hit it over a socket. Adds a process and a network hop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;libstatgrab is the right primitive for option 4: a single C library that handles the per-OS path internally (Linux &lt;code&gt;/proc&lt;/code&gt;, FreeBSD &lt;code&gt;kvm&lt;/code&gt;, macOS &lt;code&gt;host_*&lt;/code&gt; APIs) and exposes one typed surface. It has been in the Debian, Ubuntu, FreeBSD, and Homebrew package repositories for fifteen years. It just needed a PHP binding that worked on a current interpreter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What modernization meant in practice
&lt;/h2&gt;

&lt;p&gt;The 2006 binding was written against PHP 5, Zend 1, and 32-bit &lt;code&gt;long&lt;/code&gt;. Most of the rewrite is mechanical: convert TSRM-style globals, replace &lt;code&gt;Z_LVAL_PP&lt;/code&gt; patterns, switch to the typed parameter parsing macros. The non-mechanical parts were the BC quirks of the original API.&lt;/p&gt;

&lt;p&gt;Four bugs were latent in the 2006 release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stringified counters.&lt;/strong&gt; Memory totals, filesystem sizes, and CPU jiffies were returned as PHP strings. The reason was that 32-bit PHP could not hold a &lt;code&gt;uint64_t&lt;/code&gt;, so the binding called &lt;code&gt;snprintf("%lld", value)&lt;/code&gt; and shoved the string into a zval. Modern PHP runs on 64-bit &lt;code&gt;zend_long&lt;/code&gt;. The 2.0 release returns these as plain integers. Callers comparing against numeric thresholds (&lt;code&gt;if ($mem['total'] &amp;gt; 1_000_000_000)&lt;/code&gt;) now work correctly without an &lt;code&gt;intval()&lt;/code&gt; wrapping every read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swapped page-stat keys.&lt;/strong&gt; &lt;code&gt;sg_page_stats()&lt;/code&gt; returned &lt;code&gt;pages_in&lt;/code&gt; and &lt;code&gt;pages_out&lt;/code&gt; swapped. Anyone who used the function and noticed inverted memory pressure curves probably worked around it locally. Fixed in 2.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gid&lt;/code&gt; and &lt;code&gt;egid&lt;/code&gt; were copies of &lt;code&gt;uid&lt;/code&gt; and &lt;code&gt;euid&lt;/code&gt;.&lt;/strong&gt; A copy-paste in the 2006 process-stats handler. Anyone filtering by group ID had been getting user IDs back. Fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sg_user_stats()&lt;/code&gt; returned a flat list of usernames.&lt;/strong&gt; This one is a libstatgrab change, not just a binding fix. The old library exposed a &lt;code&gt;name_list&lt;/code&gt; array; the new library returns per-user records (login name, device, PID, login time, hostname). The new shape is strictly more useful. Callers reading &lt;code&gt;name_list&lt;/code&gt; migrate to reading &lt;code&gt;login_name&lt;/code&gt; from each record.&lt;/p&gt;

&lt;p&gt;The full BC catalog is in the README. None of these are surprises if you read the libstatgrab CHANGELOG; they are surprises only if you remember the 2006 binding from when you last used it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leak I didn't expect
&lt;/h2&gt;

&lt;p&gt;While running the new test suite under AddressSanitizer, statgrab's process-exit path leaked memory. Several allocations from libstatgrab's internal structures were never freed when the library shut down.&lt;/p&gt;

&lt;p&gt;This is the kind of leak that does not matter for a long-running CLI process and does not show up in a typical request-response PHP cycle (the SAPI tears down the heap on each request). It matters for ASan-clean test runs, for valgrind-clean integration tests, and for anyone embedding libstatgrab in a long-lived process where shutdown order matters.&lt;/p&gt;

&lt;p&gt;I traced it into libstatgrab itself. The library has a &lt;code&gt;sg_shutdown()&lt;/code&gt; function but several globals were not on the cleanup path. I wrote the patch and submitted it upstream against libstatgrab 0.92.1; it is pending review. The libstatgrab release cadence is slow regardless, so the statgrab repo carries a vendored copy of libstatgrab 0.92.1 with the local patch documented under &lt;code&gt;vendor/libstatgrab/LOCAL_PATCHES.md&lt;/code&gt; in the meantime.&lt;/p&gt;

&lt;p&gt;If you build with &lt;code&gt;--with-statgrab=bundled&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;vendor/libstatgrab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./configure &lt;span class="nt"&gt;--enable-static&lt;/span&gt; &lt;span class="nt"&gt;--disable-shared&lt;/span&gt; &lt;span class="nt"&gt;--without-ncurses&lt;/span&gt; &lt;span class="nt"&gt;--with-pic&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make&lt;span class="o"&gt;)&lt;/span&gt;
phpize
./configure &lt;span class="nt"&gt;--with-statgrab&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bundled
make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting &lt;code&gt;statgrab.so&lt;/code&gt; has no runtime dependency on &lt;code&gt;libstatgrab.so&lt;/code&gt;. It links the patched copy in statically. For containerized or shared-hosting deployments this matters: you do not need to install &lt;code&gt;libstatgrab&lt;/code&gt; on the target system, and you do not pick up whatever version the package manager happens to have. Once libstatgrab cuts a release with the patch, the bundled tree gets dropped or pinned to the released tarball.&lt;/p&gt;

&lt;p&gt;The legal bookkeeping: vendored libstatgrab stays LGPL 2.1+, the extension code stays PHP-3.01. Static linking does not infect the extension because LGPL explicitly permits this with the standard provisions; the LICENSE files document both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform is the actual feature
&lt;/h2&gt;

&lt;p&gt;Most of the value is right here. The same PHP code that reads CPU usage on a glibc Linux box reads it on Alpine, on macOS, on FreeBSD. No conditional based on &lt;code&gt;PHP_OS&lt;/code&gt;, no different parser per platform, no surprise when an Alpine container behaves differently from the dev box because &lt;code&gt;/proc/meminfo&lt;/code&gt; formatting differs.&lt;/p&gt;

&lt;p&gt;libstatgrab does the per-OS adaptation in C, once, with tests. Linux uses &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;sysfs&lt;/code&gt;. FreeBSD uses the &lt;code&gt;kvm&lt;/code&gt; interface. macOS uses &lt;code&gt;host_statistics()&lt;/code&gt;, &lt;code&gt;host_processor_info()&lt;/code&gt;, the BSD-style &lt;code&gt;sysctl&lt;/code&gt; tree. The binding is the same shape regardless. From the PHP side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sg_cpu_percent_usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sg_memory_stats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sg_load_stats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those three calls return populated arrays on every supported OS. There is no "if Linux, do this; else do that" in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually do with it
&lt;/h2&gt;

&lt;p&gt;The thing PHP people kept asking me about, when statgrab existed in 2006, was health endpoints. A small JSON endpoint that returns CPU usage, memory pressure, and load average so the load balancer or the orchestrator can decide whether to send traffic. Today that is more often the job of a Prometheus exporter or a sidecar agent, but the in-process version still has its place: when the application itself wants to know its own state.&lt;/p&gt;

&lt;p&gt;Concrete examples that come up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A queue worker that throttles its concurrency when load average crosses a threshold.&lt;/li&gt;
&lt;li&gt;An admin dashboard inside a long-running CLI tool showing live disk I/O and network throughput.&lt;/li&gt;
&lt;li&gt;A test harness that asserts memory stays below a budget under a synthetic workload.&lt;/li&gt;
&lt;li&gt;A graceful-shutdown hook that waits for filesystem buffers to flush before exiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each of those, shelling out is wrong (latency and parser fragility), and pulling in a stats daemon is overkill. statgrab fits the gap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sg_memory_stats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sg_load_stats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$load&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min1'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$mem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'used'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$mem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$worker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is one library call per stat, no fork, no parsing. Same code on Linux, macOS, FreeBSD.&lt;/p&gt;

&lt;p&gt;The OO surface is a thin layer for callers who prefer a class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$sg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Statgrab&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;processes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Statgrab&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SORT_CPU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$top&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'proc_name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'cpu_percent'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;PIE is the PHP Foundation's PECL successor and the recommended path on PHP 8.x:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pie &lt;span class="nb"&gt;install &lt;/span&gt;iliaal/statgrab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PECL still works for legacy installers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pecl &lt;span class="nb"&gt;install &lt;/span&gt;statgrab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From source against system libstatgrab (Debian, Ubuntu, macOS via Homebrew, FreeBSD pkg) is documented in the README. The &lt;code&gt;--with-statgrab=bundled&lt;/code&gt; path is for containerized environments and for picking up the unreleased leak fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch
&lt;/h2&gt;

&lt;p&gt;One library call instead of forking a process. One typed surface instead of a per-OS parser. The same PHP code reading CPU, memory, and load on Linux, macOS, and FreeBSD without conditionals.&lt;/p&gt;

&lt;p&gt;That is what the 2006 extension was reaching for, on PHP and libstatgrab versions that were not quite there yet. Both have caught up. The binding is the missing piece.&lt;/p&gt;

&lt;p&gt;If you run PHP on a server and have ever shelled out to &lt;code&gt;w&lt;/code&gt; or parsed &lt;code&gt;/proc/meminfo&lt;/code&gt; by hand, give it a look.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/iliaal/statgrab" rel="noopener noreferrer"&gt;https://github.com/iliaal/statgrab&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>linux</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
