mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:57:50 +03:00
docs and opus/speex submods
This commit is contained in:
165
docs/20260521_next_gen_steps_egress_review.pdf
Normal file
165
docs/20260521_next_gen_steps_egress_review.pdf
Normal file
File diff suppressed because one or more lines are too long
294
docs/AV_QUALITY_QA_SESSION.md
Normal file
294
docs/AV_QUALITY_QA_SESSION.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# AV quality enhancement — Q&A session notes
|
||||
|
||||
Research and recommendations for **movie cast** vs **video cast** quality enhancement in the Android Cast project. Combines user requirements, comparison tables, algorithm choices, placement (sender vs receiver), and fit with the current codebase.
|
||||
|
||||
**Status:** Receiver experimental presets in **Developer settings** — audio PCM chain + video GLES post-decode (immersive / HDR shaders).
|
||||
**Related code:** `app/.../receiver/av/*`, `ReceiverVideoGlRenderer`, `AudioDecoder`, `VideoDecoder` / `LibvpxVideoDecoder`, `ReceiverPlaybackActivity`, `DeveloperSettingsActivity`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Receiver A/V presets (product, May 2026)
|
||||
|
||||
Developer-only combo in **Developer settings** (not the main cast drawer):
|
||||
|
||||
| Video | Audio | Pipeline today |
|
||||
|-------|-------|----------------|
|
||||
| **Default** — decode→Surface passthrough | **Default** — decode→AudioTrack passthrough | Hook only; no DSP / no GLES |
|
||||
| **Immersive** — GLES edge-aware denoise + mild sharpen | **Bass boost** — shelf below ~150 Hz, gentle treble cut | PCM + GLES; **intensity 0–10** |
|
||||
| **HDR** — GLES Reinhard-lite + saturation | **Immersive** — clarity + noise gate + limiter (no ML v1) | RNNoise-class deferred; see §8A |
|
||||
| | **Dolby 5.1 / 7.1** — crossfeed + short delays for stereo headsets | Spatialization stub; **intensity 0–10** |
|
||||
|
||||
**Immersive audio (this pass):** clearer, wider perceived loudness without extra hiss — high-pass (~90 Hz), envelope noise gate, mild presence lift, soft ceiling. Not “cinema reverb”; aim for intelligibility on cast compression artifacts. Scale all stages by **intensity/10**.
|
||||
|
||||
**Intensity:** 0 = bypass effect (passthrough samples); 10 = maximum for that preset.
|
||||
|
||||
**Integration:** `ReceiverAvRuntime` → `AudioDecoder` after AAC decode; video decoder → intermediate OES `SurfaceTexture` → GLES shader → `TextureView` (`ReceiverVideoGlHub`) when preset is immersive/HDR; default preset uses direct decode→`TextureView` Surface.
|
||||
|
||||
---
|
||||
|
||||
## 1. User requirements (preliminary conditions)
|
||||
|
||||
1. Content is either **movie cast** or **video cast** (live screen/camera); **both** must be supported with different tuning.
|
||||
2. **Both** video and audio need quality enhancement.
|
||||
3. Two processing shapes:
|
||||
- **2.1 Temporal (N=2 frames/windows):** small gap between sequential video frames → predict/enhance using deltas between frames; same idea for audio (overlapping buffers).
|
||||
- **2.2 Single frame/chunk:** static image from sink/source; one video frame; one audio PCM chunk.
|
||||
4. **2.3 Reassembly:** output stream must keep **original PTS/DTS** (or fixed delay with uniform compensation); avoid changing frame/sample count on live paths unless explicitly designed.
|
||||
|
||||
**Comparison dimensions requested:** implementation details, difficulty, realtime on modern ARM (GPU Android/Linux where applicable), input vs output quality, single vs 2-frame vs stream (with delay), multithreading.
|
||||
|
||||
---
|
||||
|
||||
## 2. Mode overview
|
||||
|
||||
| Mode | Video input | Audio input | Typical delay | Best when |
|
||||
|------|-------------|-------------|---------------|-----------|
|
||||
| **2.2 Single** | One buffer / frame | 10–40 ms PCM chunk | **0–1 frame** (~0–33 ms @ 30 fps) | Live **video cast**, UI, games |
|
||||
| **2.1 Two-frame (N=2)** | Pair with small temporal gap | Overlapping windows | **~1 frame** (+ overlap) | Motion; mild temporal help |
|
||||
| **Stream (3–8)** | Short FIFO | Ring + STFT state | **100–400 ms+** | **Movie cast**, VOD-like |
|
||||
| **Heavy stream ML** | Long context | Streaming neural | **200 ms–2 s+** | Offline / non-live only |
|
||||
|
||||
**Quality (typical):** stream/temporal > 2-frame > single frame
|
||||
**Latency (best first):** single frame > 2-frame > stream
|
||||
|
||||
**Policy split:**
|
||||
|
||||
| Cast type | Video | Audio |
|
||||
|-----------|-------|-------|
|
||||
| **Video cast (live)** | 2.2 default; optional light 2.1 (+1 frame max) | 2.2 (**RNNoise** + limiter) |
|
||||
| **Movie cast** | 2.1 or 3–4 frame **TNR** / **MCTF-lite** | 2-window spectral or **DeepFilterNet2** streaming |
|
||||
|
||||
---
|
||||
|
||||
## 3. Video — method comparison
|
||||
|
||||
| Method | Implementation | Difficulty | ARM @ 720p30 | ARM @ 1080p30 | GPU (Android/Linux) | Quality vs input | PTS/DTS | Multithread |
|
||||
|--------|----------------|------------|--------------|---------------|---------------------|------------------|---------|-------------|
|
||||
| **Single: bilateral / guided filter denoise** | GLES/Vulkan on YUV; NEON fallback | Low | Yes | Yes (tuned) | **High** | Small–medium | Trivial | Yes (tiles) |
|
||||
| **Single: tone / CLAHE-lite** | LUT + luma histogram tiles | Low–med | Yes | Yes | Medium | Medium in flat scenes | Trivial | Yes |
|
||||
| **Single: light SR (ESPCN-class tiny CNN)** | TFLite/NCNN → NNAPI/GPU | Medium | Maybe | Tight | High | Medium (edges/text) | Trivial | Yes (async infer) |
|
||||
| **Single: heavy SR (Real-ESRGAN, SwinIR)** | Large CNN | High | No | No | BW-bound | High but slow | Trivial | Limited |
|
||||
| **2-frame: DIS / Farneback flow + warp blend** | Flow on downscaled luma → warp prev | Medium | Yes | Maybe | Med–high | Medium | **+1 frame** | Yes |
|
||||
| **2-frame: residual / delta enhance** | `enhanced = f(curr) + α·(curr − warped_prev)` | Medium | Yes | Maybe | Medium | Med–high on repetitive motion | **+1 frame** | Yes |
|
||||
| **2-frame: RIFE-lite / FILM-small** | Interpolate between frames | Med–high | Borderline 720p | Unlikely 1080p30 | High if custom | High for large gaps | Timing care | Yes |
|
||||
| **Stream: IIR TNR / motion-mask accumulate** | 3–4 frame buffer | Medium | Yes | Maybe | High | High static; trail risk | **+2–3 frames** | Yes |
|
||||
| **Stream: ML temporal (VRT/RVRT-class)** | Stateful temporal CNN | Very high | No live | No | Medium | Very high | Buffer delay | Some |
|
||||
|
||||
---
|
||||
|
||||
## 4. Audio — method comparison
|
||||
|
||||
| Method | Implementation | Difficulty | ARM realtime 48 kHz | GPU | Quality vs input | PTS/DTS | Multithread |
|
||||
|--------|----------------|------------|---------------------|-----|------------------|---------|-------------|
|
||||
| **Single: DC block + EQ + limiter** | Biquads per chunk | Low | Yes | Low | Small | Preserve samples | Yes |
|
||||
| **Single: RNNoise / DeepFilterNet (10 ms)** | TFLite/ONNX streaming | Medium | Yes mono; maybe stereo | NNAPI | High for noise | +10–20 ms | Yes (infer thread) |
|
||||
| **2-window: Wiener / min-stats STFT** | 20 ms, 50% hop | Medium | Yes | Low | Med–high | +1 hop (~10 ms) | Yes |
|
||||
| **2-window: PLC** | Predict from last 2 frames | Low–med | Yes | Low | High only on loss | Keep clock | Yes |
|
||||
| **Stream: multiband + AGC** | Ring buffer envelopes | Low–med | Yes | Low | Medium intelligibility | 5–30 ms | Yes |
|
||||
| **Stream: DeepFilterNet2** | Stateful STFT+GRU | Med–high | Yes modern ARM | NNAPI | High | 20–40 ms | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 5. Single vs 2-frame vs stream — decision matrix
|
||||
|
||||
| Criterion | Single (2.2) | 2-frame (2.1) | Stream (3–8) |
|
||||
|-----------|--------------|---------------|--------------|
|
||||
| Implementation complexity | Lowest | Medium | Highest |
|
||||
| Encoder integration | Easiest | +1 frame pipeline | Delay queue |
|
||||
| ARM realtime 720p30 | **Best** | Good | Good (TNR); poor heavy ML |
|
||||
| ARM realtime 1080p30 | Good | Marginal | Marginal |
|
||||
| GPU fit | **Best** | Good (warp) | Good (TNR) |
|
||||
| Quality gain (video) | Low–medium | Medium + motion | High static / medium action |
|
||||
| Quality gain (audio) | Medium | Med–high | High steady noise |
|
||||
| Artifact risk | Banding, oversharpen | Ghosting, warp error | Trails, smear |
|
||||
| PTS/DTS | Trivial | Fixed +1 frame delay | Uniform delay all outputs |
|
||||
| Multithread | Capture ∥ GPU ∥ encode | Flow ∥ enhance | Buffer thread + pool |
|
||||
| **Movie cast** | OK quick polish | **Recommended** | **Recommended** if delay OK |
|
||||
| **Video cast** | **Recommended** | Optional 1-frame | **Not recommended** live |
|
||||
|
||||
---
|
||||
|
||||
## 6. PTS/DTS reassembly (2.3)
|
||||
|
||||
| Approach | Description | Modes |
|
||||
|----------|-------------|-------|
|
||||
| **In-place payload replace** | Same PTS/DTS, duration; swap enhanced payload | Single |
|
||||
| **Delay compensation** | Queue enhanced frames; release with **original PTS** | 2-frame, stream |
|
||||
| **Composition timestamp** | `presentationTimeUs` from source → enhanced buffer (MediaCodec) | All on Android |
|
||||
| **Audio** | Keep **sample count** constant → PTS unchanged | Prefer for live |
|
||||
|
||||
**Rule:** Do not change frame/sample count on live cast unless explicit insert/drop policy exists.
|
||||
|
||||
---
|
||||
|
||||
## 7. Pipeline placement: sender (2.1) vs receiver (2.2)
|
||||
|
||||
### User question
|
||||
|
||||
- **2.1 Sender:** enhancement between capture (camera/screen) and encoder.
|
||||
- **2.2 Receiver:** enhancement between decoded stream and renderer.
|
||||
- **User intuition:** 2.1 has limited value because encoder lossy output destroys pre-enhance detail; increases sender load.
|
||||
|
||||
### Assessment
|
||||
|
||||
| Claim | Verdict |
|
||||
|-------|---------|
|
||||
| Encoder “kills” sharpen/SR/local contrast pushed pre-encode | **True** |
|
||||
| 2.1 is pointless | **False** for **denoise-before-encode** (helps bitrate/artifacts at same rate) |
|
||||
| 2.1 burns sender CPU for little gain | **Often true** on thermally limited phones |
|
||||
|
||||
### Preference (consensus from session)
|
||||
|
||||
| Placement | Role |
|
||||
|-----------|------|
|
||||
| **Receiver (2.2)** | **Primary perceived quality** — deblock, denoise, mild sharpen; optional 2-frame for movie |
|
||||
| **Sender (2.1)** | **Optional compression prep only** (bilateral/NLMeans denoise), not main beauty pipeline |
|
||||
| **Both** | Receiver polishes; sender light or off on live video cast |
|
||||
|
||||
**Current app hooks:**
|
||||
|
||||
| Pipeline | Integration point | Difficulty (1=easy, 5=hard) |
|
||||
|----------|-------------------|------------------------------|
|
||||
| **2.2 Video receiver** | After `VideoDecoder` / `LibvpxCapableVideoDecoder`, before `TextureView` | **3–4** (new GLES: decode → FBO → display) |
|
||||
| **2.2 Audio receiver** | `AudioDecoder` PCM before `AudioTrack` | **2** |
|
||||
| **2.1 Video sender** | Between capture and `VideoEncoder.prepare()` Surface | **4–5** (intermediate EGL Surface, rotation) |
|
||||
| **2.1 Audio sender** | Before `AudioEncoder` | **2–3** |
|
||||
| **2-frame video** | Ring buffer +1 frame delay | **+1** on above |
|
||||
|
||||
**Recommended implementation fork:** decoded-buffer → **GLES** → Surface (consistent for MediaCodec and libvpx VP9), not only `TextureView` overlay.
|
||||
|
||||
---
|
||||
|
||||
## 8. Named algorithm pipelines (best practices)
|
||||
|
||||
### A. Receiver — live video cast (default)
|
||||
|
||||
**Video (2.2, GPU-first):**
|
||||
|
||||
1. Post-decode **deblocking filter** (lite H.264-style / mild dering; VP9 loop filter already in decode)
|
||||
2. **Guided filter** or **bilateral filter** (edge-preserving denoise) on luma
|
||||
3. Mild **unsharp mask** (limit on blocky regions)
|
||||
4. Optional: **gamma** / **BT.1886** + **CLAHE-lite**
|
||||
|
||||
**Audio (2.2):**
|
||||
|
||||
1. **RNNoise** (10 ms frames)
|
||||
2. **Limiter** + high-pass (DC removal)
|
||||
3. Optional: **SpeexDSP** `preprocess` (AGC, mild denoise)
|
||||
|
||||
### B. Receiver — movie cast (+1 frame delay)
|
||||
|
||||
**Video (2.1):**
|
||||
|
||||
1. **DIS** or **Farneback** optical flow (downscaled)
|
||||
2. **MCTF-lite** or **IIR temporal denoise** on stable regions
|
||||
3. Spatial chain from A (guided/bilateral + mild unsharp)
|
||||
|
||||
**Audio (2.1):**
|
||||
|
||||
1. **STFT overlap-add** + **Wiener filter**, or **DeepFilterNet2** (streaming)
|
||||
2. **Multiband compressor**
|
||||
|
||||
### C. Sender — optional compression prep only
|
||||
|
||||
**Video:** **Bilateral** or fast **NLMeans** only when bitrate-starved — **no** Real-ESRGAN / strong sharpen pre-encode.
|
||||
|
||||
**Audio:** **SpeexDSP preprocess** or light **RNNoise** before AAC.
|
||||
|
||||
### D. Avoid for realtime in this project
|
||||
|
||||
| Algorithm | Reason |
|
||||
|-----------|--------|
|
||||
| **Real-ESRGAN**, **SwinIR**, **BasicVSR++** | Too heavy ARM @ 720p–1080p30 live |
|
||||
| **RIFE** / **FILM** @ 1080p30 full rate | Borderline; movie-only, lower rate |
|
||||
| **BM3D**, **VRT/RVRT** | Offline-tier |
|
||||
| Heavy **2.1 sender SR** before VP9 | Encoder erases; thermals |
|
||||
|
||||
---
|
||||
|
||||
## 9. Ranked preferences (quality benefit vs implementation in android cast)
|
||||
|
||||
Descending order — build in this sequence:
|
||||
|
||||
| Rank | Choice | Placement | Core algorithms | Quality benefit | Impl difficulty (this app) |
|
||||
|------|--------|-----------|-----------------|-----------------|---------------------------|
|
||||
| **1** | Receiver video **2.2** (GPU) | 2.2 | Guided/bilateral + deblock + mild unsharp | High vs blocky stream | **3–4** |
|
||||
| **2** | Receiver audio **2.2** | 2.2 | RNNoise + limiter (+ SpeexDSP) | High for noise | **2** |
|
||||
| **3** | Receiver video **2.1** (movie) | 2.2 + 1f delay | DIS/Farneback + MCTF-lite / IIR TNR + spatial | Higher static/grain | **4** |
|
||||
| **4** | Receiver audio **2.1** (movie) | 2.2 | DeepFilterNet2 or Wiener STFT | Medium–high | **3** |
|
||||
| **5** | Sender audio light prep | 2.1 | SpeexDSP / RNNoise → AAC | Low–medium | **2–3** |
|
||||
| **6** | Sender video denoise only | 2.1 | Bilateral / fast NLMeans (not SR) | Low–medium (bitrate) | **4–5** |
|
||||
| **7** | Sender video **2.1** beauty | 2.1 | Flow + enhance pre-encode | Low (washed by codec) | **5** |
|
||||
| **8** | Heavy SR either side | either | Real-ESRGAN class | High offline only | **5**, not realtime |
|
||||
|
||||
---
|
||||
|
||||
## 10. Multithreading pipeline (target architecture)
|
||||
|
||||
```
|
||||
[Capture / Network decode] → queue → [Enhance worker(s)] → queue → [Encode / Render]
|
||||
↑
|
||||
N=2: previous frame
|
||||
Stream: ring buffer
|
||||
```
|
||||
|
||||
| Stage | Threads | Notes |
|
||||
|-------|---------|-------|
|
||||
| Capture / decode callback | 1 | Non-blocking |
|
||||
| Enhance (GPU) | 1–2 | Double-buffered FBO / AHardwareBuffer |
|
||||
| CPU fallback (NEON) | 2–4 | Tile-based |
|
||||
| ML audio | 1 | Async from video |
|
||||
| Encode / display | 1 | MediaCodec async; TextureView main |
|
||||
|
||||
**Golden rule:** max **one frame** enhancement queue for **video cast**; **2–4 frames** for **movie cast**.
|
||||
|
||||
Receiver already has `videoDecodeThread` in `ReceiverCastService` — natural host for GL enhance stage.
|
||||
|
||||
---
|
||||
|
||||
## 11. Short answers to numbered conditions
|
||||
|
||||
| # | Requirement | Preference |
|
||||
|---|-------------|------------|
|
||||
| 1 | Movie or video cast | Different latency policies; same codebase, different presets |
|
||||
| 2 | Process both A+V | Shared audio chain; video policy switches by cast mode |
|
||||
| 2.1.1 | 2-frame video deltas | **DIS/Farneback + residual** — not full RIFE on ARM live |
|
||||
| 2.1.2 | 2-frame audio | Overlap-add STFT or streaming **DeepFilterNet** |
|
||||
| 2.2.1 | Single image video | **GPU bilateral/guided + tone**; tiny SR @ 720p only if ever |
|
||||
| 2.2.2 | Single chunk audio | **RNNoise + limiter** |
|
||||
| 2.3 | PTS/DTS reassembly | In-place or fixed-delay queue; no timeline shift on live |
|
||||
|
||||
---
|
||||
|
||||
## 12. Bottom line
|
||||
|
||||
| Goal | Best option |
|
||||
|------|-------------|
|
||||
| Lowest latency + ARM-safe | **Single frame receiver (2.2)** + GPU shaders |
|
||||
| Best quality per ms delay | **2-frame receiver (2.1)** + 2-window/stream audio |
|
||||
| Best absolute quality (movie) | Small **stream buffer (3–4)** video TNR + streaming audio ML |
|
||||
| Avoid realtime cast | Heavy per-frame SR, RIFE @ 1080p30, deep temporal video CNN |
|
||||
| **Placement** | **Receiver-first**; sender **denoise-only** at most |
|
||||
|
||||
**User’s encoder observation is correct for “beauty” enhancement pre-encode;** denoise-before-encode remains the only strong sender-side exception.
|
||||
|
||||
---
|
||||
|
||||
## 13. Other work in same project session (context only)
|
||||
|
||||
Unrelated to AV enhancement but done in parallel on this branch:
|
||||
|
||||
- OTA optional `.otabundle.zip`, crash reporter (`:crashwatcher`), HEADER.txt automation, PHP crash backend — see `docs/OTA.md`, `docs/CRASH_REPORTER.md`, `examples/crash_reporter/`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Open decisions when implementation starts
|
||||
|
||||
1. Enhance on **display path** (`TextureView`) vs **decode buffer → GLES → Surface** (recommended: latter).
|
||||
2. User-toggle vs automatic preset for movie vs video cast.
|
||||
3. Whether sender **denoise-only** preset is worth thermals on low-end devices.
|
||||
4. GPU API: **OpenGL ES 3.x compute** vs **Vulkan** (ES3 often faster to integrate in Android media apps).
|
||||
|
||||
---
|
||||
|
||||
*Document generated from AV quality Q&A session. Update as prototypes prove FPS/latency on target devices.*
|
||||
82
docs/GRAFANA_vs_others_graphvis_pivot.md
Normal file
82
docs/GRAFANA_vs_others_graphvis_pivot.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Android Cast — Graph Visualization Pivot
|
||||
|
||||
_Date: 2026-06-02_
|
||||
|
||||
## Context and constraints
|
||||
|
||||
- Stack: minimalistic LAMP with nginx + php-fpm + MariaDB.
|
||||
- Main constraint: **no data duplication/ETL pipeline** (no cron-based mirroring or parallel stores).
|
||||
- Need role-hierarchical analytics: user → slug admin → platform admin.
|
||||
|
||||
## Executive summary
|
||||
|
||||
- **Grafana is viable** if it can query MariaDB directly and enforce tenant/role boundaries safely.
|
||||
- **Custom PHP dashboards are safer initially** for auth/session reuse and strict slug scoping.
|
||||
- Recommended approach: **hybrid**.
|
||||
- Define a canonical SQL metrics contract once.
|
||||
- Ship custom BE dashboards first.
|
||||
- Add Grafana later only if direct DB + RBAC integration is clean.
|
||||
|
||||
## Option comparison
|
||||
|
||||
| Option | Pros | Cons / Risks | Stack impact |
|
||||
|---|---|---|---|
|
||||
| Grafana (direct DB) | Fast polished charts, panel ecosystem, alerting | Auth/RBAC integration effort; slug filtering discipline required; one more service to operate | Adds Grafana runtime (no ETL required) |
|
||||
| Custom PHP + nginx | Native shared login/session, direct BE internals, deterministic tenant logic | More UI/chart development effort | No new runtime dependencies |
|
||||
| Hybrid (recommended) | Fast practical delivery + future flexibility | Needs shared metrics contract discipline | Minimal immediate risk |
|
||||
|
||||
## Role-based graph packs (suggested)
|
||||
|
||||
### User graphs
|
||||
|
||||
- Unique devices per day (registrations/device IDs).
|
||||
- Avg cast duration by day/week.
|
||||
- Avg receive sessions by day/week.
|
||||
- Success ratio (session start vs completed).
|
||||
- App version distribution.
|
||||
|
||||
### Slug admin graphs
|
||||
|
||||
- Everything from user level, aggregated by slug.
|
||||
- Active vs passive users/devices.
|
||||
- Avg app bandwidth (send/recv) in 1d/7d windows.
|
||||
- Install source pie: Play Market vs OTA vs custom.
|
||||
- NTP/time-source usage split and correction savings metric.
|
||||
|
||||
### Platform admin graphs
|
||||
|
||||
- Everything from slug admin level, cross-slug view.
|
||||
- Crashes per slug/device/user over time.
|
||||
- Crash trend by app version / Android API / fingerprint.
|
||||
- Crash-to-ticket linkage counts with issue tracker links.
|
||||
- Top unreliable slugs/devices (rate-based).
|
||||
|
||||
## Estimates (engineering)
|
||||
|
||||
| Track | Initial delivery | Hardening | Total |
|
||||
|---|---:|---:|---:|
|
||||
| Grafana direct DB | 4-7 dev days | 5-8 dev days | 9-15 dev days |
|
||||
| Custom PHP dashboards | 5-9 dev days | 3-6 dev days | 8-15 dev days |
|
||||
| Hybrid phase-1 PHP then optional Grafana | 6-10 dev days | +4-7 dev days (optional) | 10-17 dev days |
|
||||
|
||||
_Assumptions: current MariaDB crash/ticket/session structures are available; no major schema rewrite._
|
||||
|
||||
## Recommendation under your constraints
|
||||
|
||||
1. Keep **single source of truth** in MariaDB (no ETL copy pipeline).
|
||||
2. Build **SQL metrics views/contracts** first.
|
||||
3. Implement **custom PHP dashboards** first for guaranteed auth and tenant correctness.
|
||||
4. Add Grafana later only if direct DB + RBAC mapping is validated without complexity growth.
|
||||
|
||||
## Implementation slice (proposed)
|
||||
|
||||
1. Define metrics dictionary + SQL views by role scope (1-2 days).
|
||||
2. Extend heartbeat with `time_source` + NTP correction savings fields (0.5-1 day).
|
||||
3. Build user + slug-admin dashboard pages with 1d/7d selectors (3-5 days).
|
||||
4. Build platform admin reliability board with crash/ticket links (2-3 days).
|
||||
5. Optional Grafana POC on same views (2-3 days).
|
||||
|
||||
## Source linkage
|
||||
|
||||
- This markdown file is the editable source for:
|
||||
- `tmp/GRAFANA_vs_others_graphvis_pivot.pdf`
|
||||
93
docs/GRAFANA_vs_others_graphvis_pivot.pdf
Normal file
93
docs/GRAFANA_vs_others_graphvis_pivot.pdf
Normal file
@@ -0,0 +1,93 @@
|
||||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260602143110+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260602143110+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Grafana vs Others GraphVis Pivot) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2837
|
||||
>>
|
||||
stream
|
||||
Gb!Sm=``=W&q9SYkXT2.;7oK>:J0snZUsN9b1&P`.Ku#tKU>gH;RY`>DrVrcGt_/)h0mq?'?_X>5bHrcJ/kf`a-"-#*8pJkVJdJo>Z`PnnLYgu+5qTeo&I[9P!NX3Xr"Y\Ks;A;JWKDAfWo#=$TtZ+=_L;[=Ae#%HjAre$*NN[D/d<2[rB0?`>5U,2@(<RLa"4`nO3e[H'-Z(9Ap?=%G\I2"Rg)'Xl0f1>EZ8+8AiRO0%m+<?GPP^/:N5ni=`*Z(]8PNCZ"%$3j>/XkeQ6)a'&^?CBnIo01+pNF])94'muO$\qHDHXB=um[j8MZ7660tKTn:rd^I;#U,9#T75Iuk1"9c(-b"cX'M(NqD8R!(FgaE-]GF_b+3-_DI=X)V;!g5-8+$MONCb;U\JU@R\)Q&._#tip$Hq=G;=W,WfMBXlE^:cgJ*L2,nA=-K=k=a%GJGPnhl[HaX-7l`e0/!8M-uD$17G%d+k[NLgq^4J^bN-EOK>:*ZF1J6[WA%p/ljKmObO?^VBoO&%4=(5F.\a/+]O&3b+bR?c+p>_QrFg0QM/4K<Rr\Ek1AcMcRR.=\/D9iIL4Q9ROkKZs8#,u7a>>t]]`;12_+<o-EJ'46Haj2+fV_Cc5.S#Z?ial[aVBqn0tOrqS8P:hnH6skFugnG%aZ'HU\[g7<R`L04,$]=^jt>L-sD[VTe#ir?[ab?f.0F:3kaBC,'hS?!?)$^rs/]L*EY[1jLs,G;Xk3s#g=iiSVdi9l'`F,nP"4ppWl2S<`Q@U+#rsnc/=s.s<-Z=*jdioa[Z)mS0,4]:ojt)-k@YK7Uqlp7C368C'U(-ME5]h,&JQ'(6%AK7Ru#i#`C&]7E=:bk@&Q;uRKKk+P`B9-9NFZ<2YsKoa("#R$9&"F_??oSsbt6YHCYc:3ftk79C`)n)HUgH6kVFr9'6G^mL,K^k25`(#W/,XL0o]1s^jh)J8BEgLdu0QNX[.#VmLC^I1oKG<pNV=h9t?:\3nP6M2.7V+/`5XD3cf3^^6nkr&,o)e>_ZNdLAQ8ERRHN'eJAX"EWY[W;3QGW=f`KX:RC[&9>.AFgSELIT,)LbfR>lj\L$)GO_&KN93]4AueMD\.S=6\bCCCgcGH</nO)J[:\jR,YtEJsZ3eTc^GX>Ks8eg54?X9jtFAR[%d=L0N_F,nb0P['>_r<f?N%'B0pBY>-]l*jq$7]J\`D-</3(RG;gI3q+e@J$.dOZ=/:'/gS]KkPd8,kMf,8a8e0Vi3I$$Y(]mU3mA\-'T\n"pm.G!R?cYd!",`iI"C@)7(P+#Tlu+ZpV#UkC%8\"F_ud>mi9\[PE)p@_=7<'f$4+[O6G!%5]X9cY\Bu=llBUH"#i<ETGkl-[T!_4`o^FfQtljJ0!0]R)0RR6dH/oZn9VAG"'`eQIWf*:eC9))Ac-$>tWt49O[\'"i`_horVorP;SX,B(6_<11P]kjgU@h5%[%$W#f.Mg)#9?Ob.14?IA(G.kmD!`&^.$\m1PDK-cnm=?Y;HLapQP#SDQ:(-%\S)GK*9U\rWW#*q0FonFb,RM)Zbf"HL$n[R.^Sg&,rhWbl=pl$4o.9[cdWZNfR1%"G1FO0MiLPkrd1#R%7n$BlT"DnIY#-J,m+=-1W@dfYK/!^0,&er.U']CBO"lji&JVFr9BFo,=I#FR8#_q44;^k>9(-QAo:/e`Tqm*qgV.:VZD]SjVPnENY:D4<e\,`55Tfr@aEOBT9aF`89h%gGeBM<+dE4ndrPg?gWmj5n^5jB@/$tch@[Pb*n.5G'n\d2#-hIBU<<!8\ZcmqXg&:TKC5Rg!bdca52^o?$_8hm5A`Y2Cp"rU@*U\?Hf]/;CMV=fAjFFZ'Z3JN*Yh'k_Y6m.B;J]iMFna,Z@So$RLkI]%>70=Dt3g=++4ID[Ea&QF^_recDH\&Bu.#,u64OAM&4q\qM?lou-p8f553?IlF\D#akaC>@I-A)3@LpnmnBCdOLH^NK/]q#5>4u!G;4^BG?.kcjo*SKEn6O,qo$8';A%7?aP&:n9th"ZB;\6jMg72nt/jFoRL,0++6M16]2,3A;(oS4.4CRiYaFu,8B2_/]b&`_;?;5nkqBXr\3=Zj34a*?g1g]<!Ka-(C"GcDLo`t>=ti6CcG[30(E,394MWo`DHVT@OtVBIUq.ES%OJ<eplM'0fP7F27WC1P4#c+V)K`B?l5fgh-i1t,$o;*%1B`Vs00.Z[+s0l^'2M.eua=Nm\CTtbk-L<ke$:8[.*e'#'i;V$G5V)QI".<L*+E8:_T1-`o-3_?T#j:gdA<E@s#Y.qn/AU#\8=mu<GnOg7p-93-eYfi0cSZ4f>$L`qIWR3)4]A4"co_Ro]@mDNTj.\cT5hs`k5+M-lj1H*Ee<C^5Z)VrPM"s\c!3N<un<W8SV4&g^@1pK?@7N2XUEUh[VL'0>'%cZT..mL^/SkQ[BP%BrN_Om/c=OJd',PF+)$Cn47O1)2]dK(Ni%^HOPA3S-h+mtOS?fq*m]Q!g&DN.g(3cJp$9V['Q)7ZY=A4,Hl36I[XCo4:!hr,%%k`/:cZRJZM\7bf`:(H]lnaF2!AjC*IhE&G62KC1nP<elSe4ZUJJlI"4_$BiJq5@H*MeE,h3d2FMTq/=7F1HMiN]9D)=]GY*#r7a27@oH7p=EDC!dq_#&I1QKG2Bieg<8L]:X\A1:"fPWG.AUELojW?PN/R6uqO)GsgCNm%M/-FAo-A+*Ikt02g)Z#E8(ZL_LDCpVHRE002tr-bei8,CAT/eGL'.#B:8NXNWg_:Y%?MqC5G9$r]Umk'E^5GL9ZDl`%T5,oa]Vnb*Z`9<pIp2BsU,b??R-]3o7IdcQA/boj6"h[NgZK5gC"gpatO~>endstream
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1169
|
||||
>>
|
||||
stream
|
||||
Gat=)gN)=4%"7kOi%_jCBo/!$3IT%]]=k#H\bIm4N'ob_PphonS@W'7OKrCW'1^N3`qIJ5!"M9P0SBFud!t3n^src-^P!.r=KNa*8IVY(pp-@e2%00",UEJN(kn2c,`.[$rQlO/Zein[69#+m?r^W#MP49?8mSs5KYW5]l`(Sti)\D9>#bh;]=QOSf?.4?V6]4sXM2U;ZcdiZc[-W52W?D_NVR&GP)$aQ?W5l2oK;+!.83[MWBo&M0Uo5'2A_bHG#>V@.<Ego^`Pm":b)PNfcd"pK@R)P\UZoTQMfkPcCpJ,!KZuaYa+4u&C9)CmrJ>,25S)l/T@YAeM9')>k/mNXm/u43qJAiMHbWd9DOVs^kdjulbH`H]gJpEh,!6b4_gc[fNO32:N,i1[B_>+Dp_h+h62$!E:`X*F'V&K2o<@6C1C[XCY6"!@hO@/37bGj_AIJ<k?uGU"3)Wb)pG4faK7epgLLh(0"/E/OZ@g>I*EB2^;_p_;Xm$B4EHF&q6P1NR@f$HF^ohc+P0F\(ERrW+0I6"U#M&-/CD-F>`n8J?qWsk(fN@#mfOCI!<6nB)1akT2mpI[.If?W97N\loGX58EKVJ<8@W25!_L=""A"X%.#EIB9,=<@O>NZS3-:CLo:l=]E(mPg>f%If[".g^%-jE*'>05H5%"e;J+m:&FC=flil9Ge-Yi536Xf5eL[LF?F?U*`:>H5I4^-6l)Eu$V!=?Ug9a9g@E8.AfF",u?LshMU;0E6EAT^""NX*PFa9V(;c9Ih,QM&2I&nRnHP="DLR(2#3&e%t'\$S"Z0AJlHGYq&of2@umC3hXVMYk.MJS)\Y(nKTtLKtZ!]Ek\-Zf$"&r;s(%#'iU_8/Foq[cqqPmg.h169@&<;/c(@qgH2/,kKCQ5=Ig)E7"ge4mZ>[Vc-*SBJ+[^JZHYGhRr/oVj+C5^-GdO;CBL2Q;UL6mhsB6&,T*`=?Jm>*r=a`dQ#NY7dU[H9g=D<@keA(.XhY]GEM_V`GlubcD(n1G,Sr'@Yag2pusAc9Ao6m2:T-\Gk0DKEh$N\P*j_Ihsp;d7P^FP8I06a+26#k!j@6&<X5aHY9c;WduiVampH8<5u%Q'l8pm-#?,eVaUSMuA7,X0Ht8$DL>tUToMN9i^,$9Q#C95cFq+-<rh[*]qZga5!nUY2(B~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 11
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000102 00000 n
|
||||
0000000209 00000 n
|
||||
0000000321 00000 n
|
||||
0000000524 00000 n
|
||||
0000000728 00000 n
|
||||
0000000796 00000 n
|
||||
0000001102 00000 n
|
||||
0000001167 00000 n
|
||||
0000004095 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<8697b2018a3d689f70795c513a06ba7d><8697b2018a3d689f70795c513a06ba7d>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 7 0 R
|
||||
/Root 6 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
5356
|
||||
%%EOF
|
||||
204
docs/_pdf_build/build_graphvis_pivot_pdf.py
Normal file
204
docs/_pdf_build/build_graphvis_pivot_pdf.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build tmp/GRAFANA_vs_others_graphvis_pivot.pdf decision memo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT_PDF = ROOT / "tmp" / "GRAFANA_vs_others_graphvis_pivot.pdf"
|
||||
SOURCE_MD = ROOT / "tmp" / "GRAFANA_vs_others_graphvis_pivot.md"
|
||||
|
||||
|
||||
def p(text: str, style) -> Paragraph:
|
||||
return Paragraph(text.replace("\n", "<br/>"), style)
|
||||
|
||||
|
||||
def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None:
|
||||
data = [headers] + rows
|
||||
t = Table(data, colWidths=col_widths)
|
||||
t.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 9),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(t)
|
||||
|
||||
|
||||
def build_pdf() -> None:
|
||||
styles = getSampleStyleSheet()
|
||||
title = ParagraphStyle(
|
||||
"DocTitle",
|
||||
parent=styles["Title"],
|
||||
fontSize=18,
|
||||
spaceAfter=10,
|
||||
textColor=colors.HexColor("#1565C0"),
|
||||
)
|
||||
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=12, spaceAfter=6)
|
||||
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
|
||||
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
|
||||
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=14, bulletIndent=6)
|
||||
|
||||
story: list = []
|
||||
story.append(p("Android Cast — Graph Visualization Pivot", title))
|
||||
story.append(p("Scope: LAMP + nginx backend, no data duplication pipeline, role-based analytics.", small))
|
||||
story.append(Spacer(1, 0.25 * cm))
|
||||
|
||||
add_table(
|
||||
story,
|
||||
["Field", "Value"],
|
||||
[
|
||||
["Date", "2026-06-02"],
|
||||
["Context", "apps.f0xx.org + BE Alpine (nginx + php-fpm + MariaDB)"],
|
||||
["Constraint", "No cron ETL/data copy/pipeline complexity"],
|
||||
["Output", "Decision memo: Grafana vs custom PHP dashboards"],
|
||||
["Markdown source", str(SOURCE_MD.relative_to(ROOT))],
|
||||
],
|
||||
[4.2 * cm, 12.2 * cm],
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("1. Executive summary", h1))
|
||||
story.append(
|
||||
p(
|
||||
"• Grafana is viable only if it can query MariaDB directly (or via native SQL datasource) with strict RBAC mapping and SSO/session bridging.<br/>"
|
||||
"• If direct DB + auth integration becomes complex, custom PHP dashboards are lower risk and fit your stack with zero infra additions.<br/>"
|
||||
"• Recommended path: <b>hybrid</b> — build canonical SQL views + metrics contract once; start with custom PHP pages, optionally add Grafana later reusing same SQL.",
|
||||
bullet,
|
||||
)
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("2. Option comparison", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Option", "Pros", "Cons / Risks", "Stack impact"],
|
||||
[
|
||||
[
|
||||
"Grafana (direct DB)",
|
||||
"Fast charting, polished visuals, alerting, flexible panels",
|
||||
"Session/RBAC integration effort; slug scoping must be enforced in queries; additional service to operate",
|
||||
"Adds Grafana service only (no ETL required)",
|
||||
],
|
||||
[
|
||||
"Custom PHP + nginx",
|
||||
"Native app auth/session reuse; direct BE logic access; deterministic tenant filtering",
|
||||
"More frontend/dev work for visual polish; chart UX must be implemented",
|
||||
"No new runtime dependencies",
|
||||
],
|
||||
[
|
||||
"Hybrid (recommended)",
|
||||
"Ship fast with PHP now, preserve Grafana option later; one metrics SQL contract",
|
||||
"Needs discipline in shared metrics layer",
|
||||
"Minimal immediate changes",
|
||||
],
|
||||
],
|
||||
[3.1 * cm, 4.4 * cm, 5.0 * cm, 3.9 * cm],
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("3. Role-based graph pack (suggested)", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Role", "Core dashboards", "Example charts"],
|
||||
[
|
||||
[
|
||||
"User",
|
||||
"Own slug, own devices/sessions",
|
||||
"DAU devices, avg cast duration, avg recv sessions/day, success rate, app version split",
|
||||
],
|
||||
[
|
||||
"Slug admin",
|
||||
"All slug analytics + health",
|
||||
"Active/passive users, bandwidth send/recv 1d/7d, install source pie (Play/OTA/custom), NTP source usage and correction savings",
|
||||
],
|
||||
[
|
||||
"Platform admin",
|
||||
"Cross-slug global + reliability",
|
||||
"Crashes per slug/device/user, trend by app version/SDK, links to ticket IDs/issues, top regressions by fingerprint",
|
||||
],
|
||||
],
|
||||
[2.7 * cm, 5.1 * cm, 8.6 * cm],
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("4. Effort and cost estimate (engineering)", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Track", "Initial delivery", "Hardening", "Total estimate"],
|
||||
[
|
||||
["Grafana direct DB", "4-7 dev days", "5-8 dev days", "9-15 dev days"],
|
||||
["Custom PHP dashboards", "5-9 dev days", "3-6 dev days", "8-15 dev days"],
|
||||
["Hybrid phase-1 PHP then Grafana", "6-10 dev days", "optional +4-7 days", "10-17 dev days"],
|
||||
],
|
||||
[4.3 * cm, 3.8 * cm, 3.8 * cm, 4.5 * cm],
|
||||
)
|
||||
story.append(
|
||||
p(
|
||||
"Assumptions: existing MariaDB schema with crash/ticket/session tables, role data available; excludes major schema migration.",
|
||||
small,
|
||||
)
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("5. Recommendation under your constraints", h1))
|
||||
story.append(
|
||||
p(
|
||||
"• Do not introduce ETL/duplication. Keep one source of truth: MariaDB + live SQL views.<br/>"
|
||||
"• Implement a metrics SQL layer now (views/materialized semantics via SQL only, no copied store).<br/>"
|
||||
"• Build custom PHP dashboard pages first for guaranteed auth/session + slug scoping correctness.<br/>"
|
||||
"• Re-evaluate Grafana after metrics contract stabilizes; adopt only if direct DB + RBAC mapping is clean.",
|
||||
bullet,
|
||||
)
|
||||
)
|
||||
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("6. Proposed next implementation slice", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Step", "Deliverable", "ETA"],
|
||||
[
|
||||
["1", "Define metrics dictionary + SQL views for user/slug/admin scopes", "1-2 days"],
|
||||
["2", "Add heartbeat metric: time source in use + ntp correction savings counters", "0.5-1 day"],
|
||||
["3", "Create PHP dashboards: user and slug-admin pages with 1d/7d selectors", "3-5 days"],
|
||||
["4", "Add admin reliability board + crash/ticket links", "2-3 days"],
|
||||
["5", "Optional Grafana POC over same SQL views", "2-3 days"],
|
||||
],
|
||||
[1.2 * cm, 11.9 * cm, 3.3 * cm],
|
||||
)
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
str(OUT_PDF),
|
||||
pagesize=A4,
|
||||
leftMargin=1.8 * cm,
|
||||
rightMargin=1.8 * cm,
|
||||
topMargin=1.6 * cm,
|
||||
bottomMargin=1.6 * cm,
|
||||
title="Grafana vs Others GraphVis Pivot",
|
||||
author="Android Cast project",
|
||||
)
|
||||
doc.build(story)
|
||||
print(f"Wrote {OUT_PDF}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_pdf()
|
||||
Reference in New Issue
Block a user