Files
meshcore-analyzer/cmd/server
Alex BandClaude Opus 5.5 8d2ae405e8 perf(store): cache an observation's parsed time in 8 bytes (#2196)
Refs #2148.

## Problem
`StoreObs` is the store's most numerous struct: 400k of them on a week
of a busy regional network. Its lazy timestamp cache (#1481) is a
`time.Time`, a `sync.Once` and a `bool`, together 40 bytes. On current
master that makes the struct 200 bytes, which puts every observation in
the 208-byte size class.

## Change
The cache is now a single `atomic.Int64`:
- `0` before the first call;
- the parsed time as UTC nanoseconds after it;
- or one of two sentinels: one for empty or unparseable strings, and one
for times it can't hold exactly (a non-UTC offset, or outside
`UnixNano`'s range). Those are re-parsed on each call so their location
is preserved.

Concurrent first calls may both parse; they store the same value.
`StoreObs` is now 160 bytes, in the 160-byte size class: 48 bytes × 400k
observations ≈ **19 MB** less heap. The first parse costs the same;
cached reads now build the `time.Time` from an int.

## Tests
- `TestStoreObsFitsIn160Bytes`: red commit first.
- `TestStoreObsParsedTimeMatchesParse`: Z, nanosecond, offset, SQL,
empty, garbage and epoch inputs give the same time and location as
`time.Parse`, on the first and the cached call.
- `TestStoreObsParsedTimeConcurrentFill`.
- The existing `storeobs_parsedtime_test.go` still passes.
- `go test ./cmd/server` passes, and `-race` passes on the ParsedTime,
retransmission and observer analytics tests.

## This PR or #2149
#2149 fixes #2148 by reordering these same three fields. The two
conflict, so here is how they compare. Sizes are
`unsafe.Sizeof(StoreObs{})` on each branch, with Go's heap size class
beside them:

| | struct | heap per observation | saved per observation | saved at
400k observations |
|---|---|---|---|---|
| master | 200 B | 208 B | – | – |
| #2149 (field reorder) | 192 B | 192 B | 16 B | ~6.4 MB |
| this PR (one 8-byte field) | 160 B | 160 B | 48 B | ~19 MB |

- **#2149:** a pure field reorder with no change in behaviour. It is the
smallest, safest change and the easiest to review. It saves a third of
what this PR does.
- **This PR:** three times the saving, but more code. It adds two
sentinel values, and re-parses on each call any timestamp with a non-UTC
offset. The store writes UTC (`Z`), so that path is rare. It is tested
against `time.Parse` on edge cases, concurrently, and under `-race`.

Options:
1. **Take this PR.** #2149 then has nothing left to reorder.
2. **Take #2149 now, and this PR after it** as a follow-up for the
further 32 bytes per observation. I'll rebase onto #2149 if it lands
first.
3. **Take only #2149** if the smaller, simpler change is preferred.

No config or UI changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:01:57 +02:00
..
…