Follow

js-meter

A JavaScript and jQuery profiler from 2009 — my first repository

The oldest thing here, and the first repository: js-meter, a profiling tool for HTML and JavaScript with particular attention to jQuery — measuring what a page actually does at runtime when hand-written JavaScript starts piling up.

I published it in October 2009, the week I joined GitHub, from a Taktsoft address. Everything after this page is downstream of that instinct: make the invisible measurable.


FactDetail
PurposeJavaScript / jQuery runtime profiling
ReleasedOctober 2009
ContextTaktsoft years
StatusOriginal release, kept for the record

What's in it

First repository

Published the same week the GitHub account was created — the starting point of everything else on this page.

Measure, then optimise

The same instinct as the later cache warmer, exporter and performance work.

In detail

What is actually inside it

Distribution by copy-paste

Before package managers for frontend code were the norm, a tool shipped as files in a document root. htdocs/css/images/ui-* is unmodified jQuery UI theme furniture.

First repository

Published 8 October 2009 — the week the GitHub account was created. Everything else on this page is downstream of it.

DetailValue
PurposeJavaScript / jQuery runtime profiling
Distributionfiles in a document root (`htdocs/`)
ReleasedOctober 2009

Technical notes

The decisions behind it

What was the problem you were solving in 2009?
jQuery made behaviour easy to add and hard to see: event handlers multiplying, animations stacking up. js-meter measured what a page actually did at runtime — the same instinct behind the cache warmer, the etcd exporter and the performance work that came later.

Instrumenting jQuery from the inside

238 lines that wrap the library, not the browser

In 2009 the library was the thing to measure, so jquery-profile.js wraps jQuery itself instead of sampling the browser. It is a 238-line IIFE that has to be loaded after jQuery, because the first thing it does is take a snapshot: var handle = jQuery.event.handle, ready = jQuery.ready;. From then on every dispatched DOM event goes through a replacement handler that times it, and the profile is a tree — events, each holding the jQuery calls issued while it ran.

Method capture is a loop over the prototype: for (var method in jQuery.fn) wraps every method except init and stores { name, inLength: this.length, args, duration, outLength: ret.length }. An internal flag is set while a wrapper runs, so jQuery's own nested calls are not counted twice. jQuery.fn.init gets a special case that also records the selector string and the context node — the part that answers which selector was slow. The guard compares ret.selector.substr(0, test.length) != test against the literal source of the wrapper, so the profiler never records its own code as a selector.

Events nest, so the log keeps an eventStack and a curEvent pointer. Code that runs outside any handler — the usual top-level $(...).css(...) on load — lands in a synthetic bucket called inline at log[0]. A jQuery.readyList.unshift(...) entry adds a ready pseudo-event. Events that produced no methods and took ≤1 ms are discarded with log.splice(pos, 1).

The clock is (new Date).getTime() — whole milliseconds — so that drop rule follows directly: a single fast jQuery call often rounded to zero. The tool was never going to rank micro-optimisations; it ranked aggregates and outliers. Every record also carries FunctionMonitor.currentFunction as m.fn, which is how FunMon2's named application functions attach to the jQuery calls that invoked them.

238
Lines of instrumentation
The whole profiler — one IIFE, no build step, no dependency beyond the library it wraps.
33
Files committed
~350 KB in total, roughly 250 KB of it vendored jQuery 1.3.2, jQuery UI 1.7.2 and json2.js.
20
Samples per function
FunMon2's stack_len: 20 ring buffer with hold_max: 1 keeps the slowest of the last 20 calls, not the average.
3
Commits
All three on 8 October 2009 — “first commit”, then two “initial import” messages. The work predates the repository.

From browser to report

One POST per jQuery call, aggregated in SQL

The client had no backend of its own. displayProfile() walks the captured log and, for every method, issues one $.post(FunctionMonitor.logUrl, { json: JSON.stringify(data), type: "jquery" }) — one row per jQuery call, straight from the browser to PHP. FunctionMonitor.logUrl is set by hand in the host page; in index_logging.html it is /jsprofiler/jsonlogger.php, which is also the only place the intended folder name under the document root appears.

jsonlogger.php is deliberately dumb. It decodes the JSON, uses the type request parameter as the table name, and maps the JSON keys straight onto columns, appending a timestamp. With no type everything lands in a single jsonlog(timestamp, json) table. It decodes with PEAR's Services_JSON in loose mode rather than the native json_decode, and the database path still uses the mysql_* API — mysql_connect(null, 'root', ''). Escaping is a preg_replace("/'/", "\'", ...) sitting next to a comment claiming it prevents SQL injection; the table name taken from $_REQUEST['type'] is concatenated unescaped, which it does not prevent.

report.php turns rows back into runs without any session id: it selects GROUP BY (timestamp) and declares a new profiling run whenever the gap between consecutive timestamps exceeds one second. A run's headline number is SUM(duration) over its rows. Everything else is aggregation in SQL — functions grouped by functionName, queries grouped by event, functionName, name, args, context — with percent() from util.php formatting one decimal, and shorten() clipping selectors to 30 characters (100 for arguments in detail rows) into <abbr> tooltips. Rendering is Smarty plus the jQuery UI accordion; the pie chart is a Google Image Charts URL assembled in the template, http://chart.apis.google.com/chart?cht=p&chd=t:…&chl=…&chs=700x250&chco=224499.

TableColumns, in the order jsonlogger.php maps them
`jquery``event`, `duration`, `functionName`, `methodNumber`, `name`, `args`, `inLength`, `outLength`, `selector`, `context`
`funmon``functionName`, `callNumber`, `time`, `process`, `trace`
Run boundarynot stored — report.php starts a new run when consecutive timestamps are more than 1 s apart

Shipping it as files

The install was the README

Copy files into htdocs

The README splits the tree in two. Into the openly accessible document root go db.php, index_logging.html, jsonlogger.php, report.php, util.php and the javascript/ bundle. Into the Smarty template directory go templates/reports.tpl and single_report.tpl — with the reminder to edit report.php so it can find them.

Edit db.php before anything runs

db.php is four lines of PDO settings — $dbHost, $dbName = 'jquery_log', $dbUser = 'root', $dbPass = '' — but only report.php includes it; the JSON endpoint opens its own mysql_* connection with the credentials hardcoded a second time. Nothing migrates the schema: the two CREATE TABLE statements live in a comment block in jsonlogger.php.

Two hardcoded Windows paths

report.php opens with $smarty_dir = "D:/xampp/smarty" and $jsprofiler_docroot = "D:/xampp/htdocs/jsprofiler", then points Smarty's configs, templates, templates_c and cache directories under them. Deployment meant editing those strings — there is no config file.

Pre-DevTools debugging

index_logging.html is XHTML 1.1 and loads firebug-lite-compressed.js (1.2) through an <!--[if IE]> conditional comment, because Firefox already had Firebug. The same page sets FunctionMonitor.logUrl, calls readyMaster() and leaves jQuery.displayProfile() commented out, so the visible panel is opt-in.

Loose ends in the initial release

reports.tpl links css/jquery-ui-1.7.2.custom.css, which was not committed — only the theme images, while css/profiler/report.css spells the jQuery UI Sunny palette out by hand. readyMaster() is called but defined nowhere in the repository. README~ is a zero-byte editor backup, committed with the import.

The same problem in 2026

What replaced the hook

The question js-meter answered — which selector, which handler, how many elements, how long — is a built-in now. Chrome DevTools' Performance panel records selector statistics: elapsed matching time per selector plus match attempts. performance.now() gives microsecond-scale timestamps where (new Date).getTime() gave integers, and the User Timing API (performance.mark / performance.measure) with PerformanceObserver lets a page publish its own named ranges without monkey-patching a library. Frame attribution moved from a hand-rolled JavaScript eventStack to the browser's own call trees.

The other half of js-meter — instrument in the page, POST JSON, aggregate server-side — survived as standard instrumentation. OpenTelemetry's browser instrumentation wraps fetch and XMLHttpRequest; Sentry's Browser SDK once shipped a dedicated jQuery AJAX integration. Both send spans to a collector, which is the same idea as $.post('/jsprofiler/jsonlogger.php', { json: …, type: 'jquery' }) — standardised, sampled, with a real backend instead of a Smarty template.

What did not survive is the hook. jQuery.event.handle was an internal of the 1.3-era event system and is gone from jQuery 1.9 onwards, so the wrapper no longer has anything to attach to. A 2026 version would instrument jQuery.event.dispatch or, better, leave the library alone and use the platform APIs. The trade — 1 ms clock, one row per jQuery call in MySQL, server-rendered reports — was reasonable for 2009 shared hosting, and is exactly what one would not build today.