trainmeter 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {trainmeter-0.0.2 → trainmeter-0.0.3}/PKG-INFO +10 -3
  2. {trainmeter-0.0.2 → trainmeter-0.0.3}/README.md +9 -2
  3. {trainmeter-0.0.2 → trainmeter-0.0.3}/docs/architecture.md +22 -28
  4. trainmeter-0.0.3/docs/images/dashboard-dark.png +0 -0
  5. trainmeter-0.0.3/docs/images/dashboard-light.png +0 -0
  6. {trainmeter-0.0.2 → trainmeter-0.0.3}/docs/spec.md +23 -13
  7. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/__init__.py +1 -1
  8. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/web/server.py +4 -1
  9. trainmeter-0.0.3/src/trainmeter/web/static/app.js +638 -0
  10. trainmeter-0.0.3/src/trainmeter/web/static/favicon.svg +1 -0
  11. trainmeter-0.0.3/src/trainmeter/web/static/index.html +37 -0
  12. trainmeter-0.0.3/src/trainmeter/web/static/style.css +287 -0
  13. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_dashboard.py +55 -52
  14. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_web.py +4 -0
  15. trainmeter-0.0.2/src/trainmeter/web/static/app.js +0 -571
  16. trainmeter-0.0.2/src/trainmeter/web/static/index.html +0 -43
  17. trainmeter-0.0.2/src/trainmeter/web/static/style.css +0 -157
  18. {trainmeter-0.0.2 → trainmeter-0.0.3}/.github/workflows/ci.yml +0 -0
  19. {trainmeter-0.0.2 → trainmeter-0.0.3}/.github/workflows/release.yml +0 -0
  20. {trainmeter-0.0.2 → trainmeter-0.0.3}/.gitignore +0 -0
  21. {trainmeter-0.0.2 → trainmeter-0.0.3}/CLAUDE.md +0 -0
  22. {trainmeter-0.0.2 → trainmeter-0.0.3}/LICENSE +0 -0
  23. {trainmeter-0.0.2 → trainmeter-0.0.3}/docs/hardware-checklist.md +0 -0
  24. {trainmeter-0.0.2 → trainmeter-0.0.3}/examples/README.md +0 -0
  25. {trainmeter-0.0.2 → trainmeter-0.0.3}/examples/plan_run.py +0 -0
  26. {trainmeter-0.0.2 → trainmeter-0.0.3}/examples/train_tiny_gpt.py +0 -0
  27. {trainmeter-0.0.2 → trainmeter-0.0.3}/pyproject.toml +0 -0
  28. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/__main__.py +0 -0
  29. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/__init__.py +0 -0
  30. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/bootstrap/sitecustomize.py +0 -0
  31. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/core.py +0 -0
  32. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/sender.py +0 -0
  33. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/taps.py +0 -0
  34. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/agent/wrap.py +0 -0
  35. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/catalog.py +0 -0
  36. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/cli.py +0 -0
  37. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/commands.py +0 -0
  38. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/config.py +0 -0
  39. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/doctor.py +0 -0
  40. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/emit.py +0 -0
  41. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/engine.py +0 -0
  42. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/export/__init__.py +0 -0
  43. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/export/files.py +0 -0
  44. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/export/wandb.py +0 -0
  45. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/facts.py +0 -0
  46. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/flops.py +0 -0
  47. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/metrics.py +0 -0
  48. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/passport.py +0 -0
  49. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/peaks.py +0 -0
  50. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/records.py +0 -0
  51. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/replay.py +0 -0
  52. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/report.py +0 -0
  53. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/sources/__init__.py +0 -0
  54. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/sources/gpu.py +0 -0
  55. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/sources/host.py +0 -0
  56. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/supervisor/__init__.py +0 -0
  57. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/supervisor/ingest.py +0 -0
  58. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/supervisor/launcher.py +0 -0
  59. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/supervisor/live.py +0 -0
  60. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/supervisor/store.py +0 -0
  61. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/timeline.py +0 -0
  62. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/viewer.py +0 -0
  63. {trainmeter-0.0.2 → trainmeter-0.0.3}/src/trainmeter/web/__init__.py +0 -0
  64. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_agent.py +0 -0
  65. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_cli.py +0 -0
  66. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_config.py +0 -0
  67. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_engine.py +0 -0
  68. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_examples.py +0 -0
  69. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_facts.py +0 -0
  70. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_flops.py +0 -0
  71. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_gpu.py +0 -0
  72. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_host.py +0 -0
  73. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_metrics.py +0 -0
  74. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_passport.py +0 -0
  75. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_peaks.py +0 -0
  76. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_probes_real.py +0 -0
  77. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_records.py +0 -0
  78. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_release.py +0 -0
  79. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_store.py +0 -0
  80. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_taps.py +0 -0
  81. {trainmeter-0.0.2 → trainmeter-0.0.3}/tests/test_viewer.py +0 -0
  82. {trainmeter-0.0.2 → trainmeter-0.0.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: trainmeter
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Where your training FLOPs go and how well the GPUs are used: loss, FLOPs, MFU and hardware counters for a wrapped training run.
5
5
  Project-URL: Homepage, https://github.com/Almaz-KG/trainmeter
6
6
  Project-URL: Repository, https://github.com/Almaz-KG/trainmeter
@@ -31,13 +31,20 @@ trainmeter starts your command, watches the node from outside, and serves a live
31
31
  The dashboard shows what a training run needs to be judged: training and validation loss, FLOPs spent and required, MFU with its FLOPs convention spelled out, and how busy the GPUs really are, from `GPU-Util` down to MFU, next to arithmetic intensity.
32
32
  Your training code stays as it is.
33
33
 
34
- Status: early scaffold, version 0.0.1.
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/dashboard-dark.png">
36
+ <img alt="The trainmeter dashboard: progress, throughput, MFU and OFU, goodput and the loss curve of a run on four H100s" src="docs/images/dashboard-light.png">
37
+ </picture>
38
+
39
+ <sub>The dashboard on a synthetic run.</sub>
40
+
41
+ Status: early scaffold.
35
42
  `tm` runs any command untouched, records the run, and turns what a PyTorch loop does into tokens, FLOPs spent and required, ETA, MFU (given a device) and the losses you `emit()`.
36
43
  GPU counters (NVML and GPM: the utilization ladder, OFU, observed FLOPs, arithmetic intensity) and `tm doctor` are built and tested against fakes, not yet on a real GPU.
37
44
  Series your loop already logs to wandb, TensorBoard or MLflow are picked up without a code change (`--no-tap` turns it off), and DataLoader waits, checkpoint saves and eval spans feed a "where the time goes" view.
38
45
  One real step is also counted with `FlopCounterMode` (shown beside MFU as the `counted` convention), and layers and width are read from a model config when there is one.
39
46
  Each run ends with a run passport (`passport.md` and `passport.json`) for a model card.
40
- The dashboard (Cockpit, Efficiency, Hardware, Where the time goes, Plan vs actual and Run passport) is served on localhost while the run goes; it has been checked in a browser on synthetic data, not on a real run.
47
+ The dashboard, one page of progress, tiles and charts with the run details and model card folded away, is served on localhost while the run goes; it has been checked in a browser on synthetic data, not on a real run.
41
48
  Not yet used on a real run.
42
49
  To try it, `examples/` has a tiny GPT to run under `tm` on a laptop and a run planner that needs no GPU.
43
50
 
@@ -7,13 +7,20 @@ trainmeter starts your command, watches the node from outside, and serves a live
7
7
  The dashboard shows what a training run needs to be judged: training and validation loss, FLOPs spent and required, MFU with its FLOPs convention spelled out, and how busy the GPUs really are, from `GPU-Util` down to MFU, next to arithmetic intensity.
8
8
  Your training code stays as it is.
9
9
 
10
- Status: early scaffold, version 0.0.1.
10
+ <picture>
11
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/dashboard-dark.png">
12
+ <img alt="The trainmeter dashboard: progress, throughput, MFU and OFU, goodput and the loss curve of a run on four H100s" src="docs/images/dashboard-light.png">
13
+ </picture>
14
+
15
+ <sub>The dashboard on a synthetic run.</sub>
16
+
17
+ Status: early scaffold.
11
18
  `tm` runs any command untouched, records the run, and turns what a PyTorch loop does into tokens, FLOPs spent and required, ETA, MFU (given a device) and the losses you `emit()`.
12
19
  GPU counters (NVML and GPM: the utilization ladder, OFU, observed FLOPs, arithmetic intensity) and `tm doctor` are built and tested against fakes, not yet on a real GPU.
13
20
  Series your loop already logs to wandb, TensorBoard or MLflow are picked up without a code change (`--no-tap` turns it off), and DataLoader waits, checkpoint saves and eval spans feed a "where the time goes" view.
14
21
  One real step is also counted with `FlopCounterMode` (shown beside MFU as the `counted` convention), and layers and width are read from a model config when there is one.
15
22
  Each run ends with a run passport (`passport.md` and `passport.json`) for a model card.
16
- The dashboard (Cockpit, Efficiency, Hardware, Where the time goes, Plan vs actual and Run passport) is served on localhost while the run goes; it has been checked in a browser on synthetic data, not on a real run.
23
+ The dashboard, one page of progress, tiles and charts with the run details and model card folded away, is served on localhost while the run goes; it has been checked in a browser on synthetic data, not on a real run.
17
24
  Not yet used on a real run.
18
25
  To try it, `examples/` has a tiny GPT to run under `tm` on a laptop and a run planner that needs no GPU.
19
26
 
@@ -341,42 +341,36 @@ Overlays are computed from the primitives at view time and never stored.
341
341
 
342
342
  The dashboard is one static page fed by the read-only API of section 10.
343
343
  It renders from the catalog, so a new series appears without a page change.
344
- The views, the rendering rules and the x-axis switch are specified in `docs/spec.md` (Screens).
344
+ The page, the rendering rules and the x-axis switch are specified in `docs/spec.md` (Screens).
345
345
  Plotting loss against FLOPs makes runs comparable across batch sizes and sequence lengths.
346
- Cockpit, Efficiency and Hardware arrive in slice 4, Where the time goes in slice 5, and the run passport and Plan vs actual in slice 7.
346
+ The views of slices 4, 5 and 7 were later folded into this one page without tabs.
347
347
 
348
- Layout sketches, with placeholders instead of numbers:
348
+ Layout sketch, with placeholders instead of numbers:
349
349
 
350
350
  ```text
351
351
  +----------------------------------------------------------------------+
352
- | <command> | <n> x <GPU> | FLOPs per token <value> (<convention>) |
353
- +----------------------------------+-----------------------------------+
354
- | PROGRESS | THROUGHPUT |
355
- | FLOPs spent of FLOPs required | tokens/s and step time |
356
- | tokens seen, ETA | data wait share of the step |
357
- +----------------------------------+-----------------------------------+
358
- | TRAIN LOSS and VAL LOSS | MFU and OFU |
359
- | x axis: step, tokens, FLOPs | the gap between them |
360
- | or wall time | latest utilization ladder |
361
- +----------------------------------+-----------------------------------+
362
- | LR, GRAD NORM and every other series the loop logs |
352
+ | trainmeter <n> x <GPU> [step|tokens|FLOPs|time] <state>|
353
+ +----------------------------------------------------------------------+
354
+ | PROGRESS <percent> tokens, FLOPs of required, ETA, elapsed |
355
+ +-----------+-----------+-----------+-----------+-----------+----------+
356
+ | tokens/s | step time | MFU | OFU | goodput | FLOPs/tok|
357
+ | sparkline | sparkline | sparkline | sparkline | sparkline | |
358
+ +-----------+-----------+-----------+-----------+-----------+----------+
359
+ | TRAINING: loss (full width), tokens/s, MFU and OFU, lr, grad norm, |
360
+ | every other logged series, plan against actual |
361
+ +----------------------------------------------------------------------+
362
+ | GPU EFFICIENCY: ladder now and over time, roofline, arithmetic |
363
+ | intensity, FLOPs spent (modeled next to observed) |
364
+ +----------------------------------------------------------------------+
365
+ | NODE: time split, host CPU and memory |
366
+ | GPUS: one small chart per counter, one line per GPU; ranks |
367
+ +----------------------------------------------------------------------+
368
+ | > Run details (collapsed) > Model card (collapsed) |
363
369
  +----------------------------------------------------------------------+
364
370
  ```
365
371
 
366
- ```text
367
- +----------------------------------+-----------------------------------+
368
- | UTILIZATION LADDER over time | ROOFLINE |
369
- | GPU-Util, SM active, | arithmetic intensity against |
370
- | tensor active, OFU, MFU | achieved FLOP/s, with the ridge |
371
- | | and the peak of the device |
372
- +----------------------------------+-----------------------------------+
373
- | FLOPs SPENT | ARITHMETIC INTENSITY over time |
374
- | modeled next to observed | aggregate over the window |
375
- +----------------------------------+-----------------------------------+
376
- ```
377
-
378
- The optional price field is the one place where money exists.
379
- It computes dollars in the browser from `elapsed`, `gpu_hours` and `tokens_seen`, keeps the price in that browser, and never sends it to the supervisor or writes it to the run log.
372
+ The dashboard shows no price and no dollars.
373
+ Money stays an overlay over `elapsed`, `gpu_hours` and `tokens_seen`, and a price never reaches the supervisor or the run log.
380
374
 
381
375
  ## 8. Run lifecycle
382
376
 
@@ -73,7 +73,7 @@ Rules:
73
73
 
74
74
  1. Unknown stays unknown.
75
75
  A metric that lacks an input is absent and names the input that would enable it, and a zero is never shown in its place.
76
- 2. An assumed fact carries a visible badge.
76
+ 2. An assumed fact is marked `assumed` in the run details, and the dashboard raises a notice for it.
77
77
  A declared value that disagrees with a detected one is used, and a warning shows both.
78
78
  3. `n_gpus` is the number of GPUs the run uses, taken from the ranks.
79
79
  Falling back to the visible devices is an assumed fact with a warning.
@@ -107,7 +107,7 @@ The engine maps the raw key the loop used to a canonical series:
107
107
  | `lr` | `lr`, `learning_rate`, `train/lr`, `train/learning_rate` |
108
108
  | `grad_norm` | `grad_norm`, `train/grad_norm`, `gradient_norm` |
109
109
 
110
- A mapped series carries a `mapped by name` badge and its original key.
110
+ A mapped series shows its original key, with `mapped by name` in its tooltip.
111
111
  `tm.toml` can pin or override a mapping.
112
112
  A number that matches no alias still appears as a custom series under its own key.
113
113
 
@@ -186,7 +186,7 @@ Across ranks: the spread of step time, for straggler detection.
186
186
  Overlays are computed from the primitives at view time and never stored in the run log.
187
187
  Dollars are GPU-hours times a price per GPU-hour, or elapsed hours times a price per node-hour.
188
188
  GPU energy is the integral of the sampled power.
189
- A price is typed into the dashboard, stays in that browser, and is never sent to the supervisor.
189
+ The dashboard shows no dollars, and a price is never sent to the supervisor.
190
190
 
191
191
  ## Run log
192
192
 
@@ -257,23 +257,33 @@ The run ends when the program does, with no exit status, because `tm` did not st
257
257
 
258
258
  ## Screens
259
259
 
260
- | Screen | Shows | Answers |
260
+ The dashboard is one page with no tabs.
261
+ Its header holds only the icon, the rig (GPU count and model), the x-axis switch and the run state.
262
+ Below it, top to bottom:
263
+
264
+ | Part | Shows | Answers |
261
265
  | --- | --- | --- |
262
- | Cockpit | progress (FLOPs spent of required, tokens, ETA), train and validation loss, tokens/s and step time, MFU and OFU with their gap, lr, grad norm and every other logged series | Is the run on plan? |
263
- | Efficiency | the utilization ladder over time, MFU against OFU, arithmetic intensity over time, the roofline point, FLOPs spent (modeled next to observed) | Are the GPUs used well, and where does the gap open? |
264
- | Hardware | per GPU: tensor, DRAM and SM active, power, clocks, memory, temperature, NVLink, stragglers; and the process tree's CPU (cores busy) and resident memory | Which card is slow? |
265
- | Where the time goes | node time split: boot, startup and compile, data, compute, NCCL, checkpoints, eval, idle | What did the node spend time on without training? |
266
- | Run passport | architecture, FLOPs per token, hardware, topology, versions, final losses | What goes into the model card? |
267
- | Plan vs actual | the declared budget and sizing assumptions (MFU, time) next to measured values | How wrong was my plan? |
266
+ | Progress | percent of the token budget, tokens and FLOPs spent of required, ETA, elapsed and GPU-hours | Is the run on plan? |
267
+ | Tiles | tokens/s, step time, MFU, OFU, goodput and FLOPs per token, each a number with a sparkline | How is it going right now? |
268
+ | Training | train and validation loss, tokens/s, MFU against OFU, lr, grad norm, every other logged series, and plan against actual | Is the model learning, and how wrong was my plan? |
269
+ | GPU efficiency | the utilization ladder now and over time, the roofline point, arithmetic intensity over time, FLOPs spent (modeled next to observed) | Are the GPUs used well, and where does the gap open? |
270
+ | Node | node time split (startup and compile, data, checkpoints, eval, compute), and the process tree's CPU (cores busy) and resident memory | What did the node spend time on without training? |
271
+ | GPUs | per GPU: tensor, DRAM and SM active, power, clocks, memory, temperature, NVLink, and step time per rank | Which card is slow? |
272
+ | Run details (collapsed) | the command, host, directory, declared flags, model, FLOPs per token under every convention, hardware and versions | What exactly ran? |
273
+ | Model card (collapsed) | the run passport as Markdown, with a copy button | What goes into the model card? |
274
+
275
+ A chart with no data is left out, and a part with nothing to draw is one line that names what would fill it.
268
276
 
269
277
  Rendering rules:
270
278
 
271
- 1. Every number shows its convention and provenance, and `assumed` and `mapped by name` carry a visible badge.
272
- 2. A metric that lacks an input names the input that would enable it, such as "budget not declared: pass --budget-tokens", and never shows a zero.
279
+ 1. Every MFU and FLOPs number shows its convention next to it in plain text, and a series mapped by name shows its raw key.
280
+ Provenance (`declared`, `assumed`, `measured`) is listed next to each fact in the run details, and a guessed input also raises a notice at the top of the page.
281
+ A tile is a label, a number, one short line and a sparkline, with no badges; any longer explanation is in its tooltip.
282
+ 2. A metric that lacks an input shows "n/a", names the input that would enable it in its tooltip, such as "budget not declared: pass --budget-tokens", and never shows a zero.
273
283
  3. Arithmetic intensity is labeled as an aggregate over the sampling window.
274
284
  4. Every time series has an x-axis switch: step, tokens, FLOPs spent (modeled, with its convention shown) or wall time.
275
285
  Until a step has been seen, and for a program watched from outside, the charts use wall time, because every point would otherwise sit at zero.
276
- 5. The header has an optional price field that adds dollars spent and dollars per billion tokens, as described under Overlays.
286
+ 5. The dashboard shows no price and no dollars.
277
287
  6. The dashboard is static files with hand-written SVG charts, no build step and no CDN.
278
288
 
279
289
  ## MVP
@@ -4,7 +4,7 @@ from .emit import emit
4
4
  from .flops import CONVENTIONS, ModelShape, all_conventions, flops_per_token
5
5
  from .peaks import Peak, lookup
6
6
 
7
- __version__ = "0.0.2"
7
+ __version__ = "0.0.3"
8
8
 
9
9
  __all__ = [
10
10
  "CONVENTIONS",
@@ -1,6 +1,6 @@
1
1
  """The dashboard server: standard library only, read only, loopback only.
2
2
 
3
- `GET /` and its three static files, and a small JSON API over `LiveState`. There is no endpoint
3
+ `GET /`, its static files and the icon, and a small JSON API over `LiveState`. There is no endpoint
4
4
  that changes anything, other methods get 405, and a Host header that is not the loopback name and
5
5
  this port is refused, which closes DNS rebinding. Binding beyond loopback is not offered.
6
6
  """
@@ -25,6 +25,9 @@ FILES = {
25
25
  "/": ("index.html", "text/html; charset=utf-8"),
26
26
  "/app.js": ("app.js", "text/javascript; charset=utf-8"),
27
27
  "/style.css": ("style.css", "text/css; charset=utf-8"),
28
+ "/favicon.svg": ("favicon.svg", "image/svg+xml"),
29
+ # Asked for by clients that ignore the <link>; every current browser renders an SVG here.
30
+ "/favicon.ico": ("favicon.svg", "image/svg+xml"),
28
31
  }
29
32
  DEFAULT_PORT = 8765
30
33
  STREAM_INTERVAL = 1.0