feat(playground): benchmark de rendimiento con flags JetStream/E2E/payload
Añade GET /api/bench (SSE) y una seccion de simulador en index.html: un publisher inunda una room con miles de mensajes a N subscribers y una grafica en vivo anima el throughput. Las dos politicas de room se exponen como flags independientes (persist=JetStream, encrypt=E2E AEAD+Ed25519) mas tamano de payload, midiendo el coste de cada capa con la libreria cliente real. El benchmark usa peers efimeros propios, sin tocar los peers nombrados del sandbox manual. Verificado: las 4 combinaciones enc x persist con fan-out exacto. Bump app v0.2.0.
This commit is contained in:
@@ -72,6 +72,40 @@ Cleartext rooms (leave the checkbox unticked) behave like plain NATS fan-out:
|
||||
fast, ephemeral, unsigned. Encrypted rooms are the Matrix-like mode: E2E
|
||||
encrypted, persisted, and per-message signed.
|
||||
|
||||
## Benchmark: throughput simulator
|
||||
|
||||
The bottom panel of the UI is a performance simulator. Press **▶ Ejecutar
|
||||
benchmark** and one publisher floods a fresh room with thousands of messages
|
||||
that N subscribers receive (fan-out); a live canvas chart animates the sent vs
|
||||
received totals while it runs.
|
||||
|
||||
The two policy axes are exposed as **independent flags**, so the benchmark
|
||||
measures the cost of each layer in isolation:
|
||||
|
||||
| JetStream | Encryption | Room policy | What it costs |
|
||||
|---|---|---|---|
|
||||
| off | off | `{Encrypt:false, Persist:false}` | plain core NATS fan-out |
|
||||
| **on** | off | `{Encrypt:false, Persist:true}` | durable JetStream (publish ack per message) |
|
||||
| off | **on** | `{Encrypt:true, Persist:false}` | AEAD + Ed25519 signature per message, core transport |
|
||||
| **on** | **on** | `{Encrypt:true, Persist:true}` | full E2E + durable history |
|
||||
|
||||
A **payload size** slider (16 B – 8 KiB) sets the message size. Encrypted or
|
||||
persistent runs are capped to 30 000 messages (each message pays per-message
|
||||
crypto and/or a JetStream ack, so they run much slower than plain NATS).
|
||||
|
||||
The benchmark uses its own ephemeral peers (fresh identities, never persisted),
|
||||
so it never touches the named peers of the manual sandbox.
|
||||
|
||||
It is driven by an SSE endpoint that streams progress samples:
|
||||
|
||||
```bash
|
||||
curl -N "http://localhost:7700/api/bench?n_msgs=20000&n_subs=3&payload=128&encrypt=0&persist=0"
|
||||
# emits: data: {"type":"start",...} data: {"type":"sample",...} data: {"type":"done",...}
|
||||
```
|
||||
|
||||
Query params: `n_msgs`, `n_subs` (1–16), `payload` (bytes), `encrypt` (0/1),
|
||||
`persist` (0/1).
|
||||
|
||||
## State / cleanup
|
||||
|
||||
All writable state lives under `playground/local_files/`:
|
||||
|
||||
@@ -221,6 +221,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BENCHMARK: full-width performance simulator -->
|
||||
<div style="padding: 0 20px 32px; max-width: 1200px;">
|
||||
<div class="card">
|
||||
<h2>Benchmark de rendimiento · 1 publisher → N subscribers</h2>
|
||||
<div style="display:flex; gap:26px; flex-wrap:wrap; align-items:flex-end; margin-bottom:6px;">
|
||||
<div style="min-width:230px;">
|
||||
<label>Mensajes a publicar · <span id="bMsgsVal" style="color:var(--fg)">20 000</span></label>
|
||||
<input id="bMsgs" type="range" min="1000" max="200000" step="1000" value="20000" style="width:100%; accent-color:var(--accent);" />
|
||||
</div>
|
||||
<div style="min-width:160px;">
|
||||
<label>Subscribers · <span id="bSubsVal" style="color:var(--fg)">3</span></label>
|
||||
<input id="bSubs" type="range" min="1" max="16" step="1" value="3" style="width:100%; accent-color:var(--accent);" />
|
||||
</div>
|
||||
<div style="min-width:200px;">
|
||||
<label>Tamaño payload · <span id="bPayVal" style="color:var(--fg)">128 B</span></label>
|
||||
<input id="bPay" type="range" min="16" max="8192" step="16" value="128" style="width:100%; accent-color:var(--accent);" />
|
||||
</div>
|
||||
<div class="checkrow" style="margin:0;">
|
||||
<input id="bPersist" type="checkbox" />
|
||||
<label for="bPersist">🗂 JetStream (persistente)</label>
|
||||
</div>
|
||||
<div class="checkrow" style="margin:0;">
|
||||
<input id="bEncrypt" type="checkbox" />
|
||||
<label for="bEncrypt">🔒 Encriptación E2E</label>
|
||||
</div>
|
||||
<button id="bRun" style="margin:0;">▶ Ejecutar benchmark</button>
|
||||
</div>
|
||||
<div class="help" style="margin:6px 0 12px;">
|
||||
<b>JetStream</b> y <b>Encriptación</b> son ejes independientes: NATS core (ambos off) · JetStream durable · E2E (AEAD + firma Ed25519 por mensaje) · E2E + JetStream. Los modos con cripto o persistencia se limitan a 30 000 mensajes (cada mensaje paga cifrado/firma/ack).
|
||||
</div>
|
||||
<div style="display:flex; gap:30px; flex-wrap:wrap; margin:4px 2px 8px;">
|
||||
<div><div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em;">Enviados</div><div id="bSent" style="font-size:22px; color:var(--accent);">0</div></div>
|
||||
<div><div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em;">Recibidos (Σ subs)</div><div id="bRecv" style="font-size:22px; color:var(--green);">0</div></div>
|
||||
<div><div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em;">Throughput recv</div><div id="bTps" style="font-size:22px; color:var(--gold);">0</div></div>
|
||||
<div><div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em;">Tiempo</div><div id="bTime" style="font-size:22px;">0.00 s</div></div>
|
||||
</div>
|
||||
<canvas id="bChart" style="width:100%; height:300px; display:block; background:#08090c; border:1px solid var(--border); border-radius:8px;"></canvas>
|
||||
<div style="display:flex; gap:18px; font-size:12px; color:var(--muted); margin-top:6px;">
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--accent);margin-right:6px;"></span>enviados (publisher)</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--green);margin-right:6px;"></span>recibidos (suma de subscribers)</span>
|
||||
</div>
|
||||
<div id="bStatus" class="status" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
@@ -452,6 +497,98 @@ $("kickBtn").onclick = async () => {
|
||||
|
||||
$("refreshPeersBtn").onclick = refreshPeers;
|
||||
$("peerName").addEventListener("keydown", (e) => { if (e.key === "Enter") $("connectBtn").click(); });
|
||||
|
||||
// ---- benchmark ----
|
||||
const fmtN = (n) => Number(n).toLocaleString("es-ES");
|
||||
const bMsgs = $("bMsgs"), bSubs = $("bSubs"), bPay = $("bPay");
|
||||
bMsgs.oninput = () => $("bMsgsVal").textContent = fmtN(+bMsgs.value);
|
||||
bSubs.oninput = () => $("bSubsVal").textContent = bSubs.value;
|
||||
bPay.oninput = () => $("bPayVal").textContent = fmtN(+bPay.value) + " B";
|
||||
|
||||
let bSamples = [], bRunning = false, bES = null;
|
||||
const bCanvas = $("bChart"), bCtx = bCanvas.getContext("2d");
|
||||
function cssVar(n) { return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); }
|
||||
|
||||
function bResize() {
|
||||
const dpr = window.devicePixelRatio || 1, r = bCanvas.getBoundingClientRect();
|
||||
bCanvas.width = r.width * dpr; bCanvas.height = r.height * dpr;
|
||||
bCtx.setTransform(dpr, 0, 0, dpr, 0, 0); bDraw();
|
||||
}
|
||||
window.addEventListener("resize", bResize);
|
||||
|
||||
function bDraw() {
|
||||
const r = bCanvas.getBoundingClientRect(), W = r.width, H = r.height;
|
||||
const padL = 70, padR = 14, padT = 12, padB = 26;
|
||||
bCtx.clearRect(0, 0, W, H);
|
||||
const tMax = bSamples.length ? Math.max(bSamples[bSamples.length - 1].t, 0.001) : 1;
|
||||
const yMax = bSamples.length ? Math.max(...bSamples.map(s => Math.max(s.sent, s.recv)), 1) : 1;
|
||||
bCtx.strokeStyle = "#2b333f"; bCtx.fillStyle = "#8b98a5"; bCtx.font = "11px ui-monospace";
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const yy = (H - padB) - (i / 5) * (H - padT - padB);
|
||||
bCtx.beginPath(); bCtx.moveTo(padL, yy); bCtx.lineTo(W - padR, yy); bCtx.stroke();
|
||||
bCtx.textAlign = "right"; bCtx.fillText(fmtN(Math.round((i / 5) * yMax)), padL - 8, yy + 3);
|
||||
}
|
||||
bCtx.textAlign = "center";
|
||||
bCtx.fillText("0 s", padL, H - padB + 15);
|
||||
bCtx.fillText(tMax.toFixed(2) + " s", W - padR, H - padB + 15);
|
||||
if (bSamples.length < 2) return;
|
||||
const x = (t) => padL + (t / tMax) * (W - padL - padR);
|
||||
const y = (v) => (H - padB) - (v / yMax) * (H - padT - padB);
|
||||
const line = (key, color) => {
|
||||
bCtx.beginPath(); bCtx.lineWidth = 2.2; bCtx.strokeStyle = color;
|
||||
bSamples.forEach((s, i) => { const px = x(s.t), py = y(s[key]); i ? bCtx.lineTo(px, py) : bCtx.moveTo(px, py); });
|
||||
bCtx.stroke();
|
||||
};
|
||||
line("sent", cssVar("--accent"));
|
||||
line("recv", cssVar("--green"));
|
||||
}
|
||||
|
||||
function bSetRunning(v) { bRunning = v; $("bRun").disabled = v; }
|
||||
|
||||
$("bRun").onclick = () => {
|
||||
if (bRunning) return;
|
||||
bSamples = []; bSetRunning(true);
|
||||
$("bSent").textContent = "0"; $("bRecv").textContent = "0"; $("bTps").textContent = "0"; $("bTime").textContent = "0.00 s";
|
||||
setStatus("bStatus", "conectando…");
|
||||
const qs = new URLSearchParams({
|
||||
n_msgs: bMsgs.value, n_subs: bSubs.value, payload: bPay.value,
|
||||
encrypt: $("bEncrypt").checked ? "1" : "0", persist: $("bPersist").checked ? "1" : "0",
|
||||
});
|
||||
const es = new EventSource("/api/bench?" + qs.toString());
|
||||
bES = es;
|
||||
const finish = () => { try { es.close(); } catch (_) {} bSetRunning(false); };
|
||||
es.addEventListener("end", finish);
|
||||
es.onmessage = (e) => {
|
||||
let m; try { m = JSON.parse(e.data); } catch (_) { return; }
|
||||
if (m.type === "start") {
|
||||
setStatus("bStatus",
|
||||
"corriendo… " + fmtN(m.n_msgs) + " msgs → " + m.n_subs + " subs · payload " + fmtN(m.payload) + "B"
|
||||
+ (m.encrypt ? " · \u{1F512} E2E" : "") + (m.persist ? " · \u{1F5C4} JetStream" : "")
|
||||
+ (m.capped ? " · (limitado a 30k)" : ""), "");
|
||||
} else if (m.type === "sample") {
|
||||
bSamples.push({ t: m.t, sent: m.sent, recv: m.recv });
|
||||
$("bSent").textContent = fmtN(m.sent); $("bRecv").textContent = fmtN(m.recv); $("bTime").textContent = m.t.toFixed(2) + " s";
|
||||
if (bSamples.length >= 2) {
|
||||
const a = bSamples[bSamples.length - 2], b = bSamples[bSamples.length - 1], dt = b.t - a.t;
|
||||
if (dt > 0) $("bTps").textContent = fmtN(Math.round((b.recv - a.recv) / dt));
|
||||
}
|
||||
bDraw();
|
||||
} else if (m.type === "done") {
|
||||
bSamples.push({ t: m.t, sent: m.sent, recv: m.recv });
|
||||
$("bSent").textContent = fmtN(m.sent); $("bRecv").textContent = fmtN(m.recv);
|
||||
$("bTps").textContent = fmtN(m.recv_tps); $("bTime").textContent = m.t.toFixed(2) + " s";
|
||||
setStatus("bStatus",
|
||||
"✓ " + m.t.toFixed(2) + "s · pub " + fmtN(m.pub_tps) + "/s · recv " + fmtN(m.recv_tps) + "/s · fan-out ×"
|
||||
+ m.n_subs + " · por sub [" + (m.per_sub || []).map(fmtN).join(", ") + "]", "ok");
|
||||
bDraw(); finish();
|
||||
} else if (m.type === "error") {
|
||||
setStatus("bStatus", "error: " + m.msg, "bad"); finish();
|
||||
}
|
||||
};
|
||||
es.onerror = () => { if (bRunning) { setStatus("bStatus", "conexión SSE perdida", "bad"); finish(); } };
|
||||
};
|
||||
|
||||
bResize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -28,12 +29,15 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
cs "fn-registry/functions/cybersecurity"
|
||||
"github.com/enmanuel/unibus/pkg/blobstore"
|
||||
"github.com/enmanuel/unibus/pkg/client"
|
||||
"github.com/enmanuel/unibus/pkg/embeddednats"
|
||||
@@ -498,6 +502,257 @@ func (h *Hub) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmark: one publisher floods a room with thousands of messages that N
|
||||
// subscribers receive. The two policy axes are exposed as independent flags:
|
||||
// encrypt (AEAD payload + Ed25519 per-message signature) and persist (durable
|
||||
// JetStream history vs ephemeral core NATS). Payload size is configurable. The
|
||||
// benchmark uses its own ephemeral peers (not the hub's named peers) so it never
|
||||
// interferes with the manual sandbox, and streams progress samples over SSE so
|
||||
// the browser can animate a live throughput chart.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// benchSample is one Server-Sent Event of a running benchmark.
|
||||
type benchSample struct {
|
||||
Type string `json:"type"` // "start" | "sample" | "done" | "error"
|
||||
T float64 `json:"t"`
|
||||
Sent int64 `json:"sent"`
|
||||
Recv int64 `json:"recv"`
|
||||
NMsgs int `json:"n_msgs,omitempty"`
|
||||
NSubs int `json:"n_subs,omitempty"`
|
||||
Payload int `json:"payload,omitempty"`
|
||||
Encrypt bool `json:"encrypt,omitempty"`
|
||||
Persist bool `json:"persist,omitempty"`
|
||||
Capped bool `json:"capped,omitempty"`
|
||||
PubTps int64 `json:"pub_tps,omitempty"`
|
||||
RecvTps int64 `json:"recv_tps,omitempty"`
|
||||
PerSub []int64 `json:"per_sub,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
}
|
||||
|
||||
// runBench wires up one publisher + nSubs subscribers, publishes nMsgs payloads,
|
||||
// and calls emit periodically with the running totals. emit is only ever called
|
||||
// from the calling goroutine (the SSE handler), so it needs no locking.
|
||||
func runBench(ctx context.Context, emit func(benchSample), nMsgs, nSubs, payloadBytes int, encrypt, persist bool) {
|
||||
policy := room.Policy{Encrypt: encrypt, Persist: persist, SignMsgs: encrypt}
|
||||
subject := fmt.Sprintf("bench.%d", time.Now().UnixNano())
|
||||
|
||||
newPeer := func() (*client.Client, error) {
|
||||
id, err := cs.GenerateIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.New(natsURL, ctrlURL, id)
|
||||
}
|
||||
|
||||
pub, err := newPeer()
|
||||
if err != nil {
|
||||
emit(benchSample{Type: "error", Msg: "publisher: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer pub.Close()
|
||||
|
||||
roomID, err := pub.CreateRoom(subject, policy)
|
||||
if err != nil {
|
||||
emit(benchSample{Type: "error", Msg: "create room: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
counters := make([]int64, nSubs)
|
||||
subClients := make([]*client.Client, 0, nSubs)
|
||||
defer func() {
|
||||
for _, c := range subClients {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// One room, N subscribers. For encrypted rooms each subscriber must be invited
|
||||
// (sealed key) and join before subscribing; for cleartext rooms Subscribe on
|
||||
// the shared roomID is enough.
|
||||
for i := 0; i < nSubs; i++ {
|
||||
c, err := newPeer()
|
||||
if err != nil {
|
||||
emit(benchSample{Type: "error", Msg: fmt.Sprintf("subscriber %d: %v", i, err)})
|
||||
return
|
||||
}
|
||||
subClients = append(subClients, c)
|
||||
if encrypt {
|
||||
if err := pub.Invite(roomID, c.Endpoint()); err != nil {
|
||||
emit(benchSample{Type: "error", Msg: fmt.Sprintf("invite %d: %v", i, err)})
|
||||
return
|
||||
}
|
||||
if err := c.Join(roomID); err != nil {
|
||||
emit(benchSample{Type: "error", Msg: fmt.Sprintf("join %d: %v", i, err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
idx := i
|
||||
if _, err := c.Subscribe(roomID, func(_ frame.Frame, _ []byte) {
|
||||
atomic.AddInt64(&counters[idx], 1)
|
||||
}); err != nil {
|
||||
emit(benchSample{Type: "error", Msg: fmt.Sprintf("subscribe %d: %v", i, err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sumRecv := func() int64 {
|
||||
var s int64
|
||||
for i := range counters {
|
||||
s += atomic.LoadInt64(&counters[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
payload := bytes.Repeat([]byte{'x'}, payloadBytes)
|
||||
var sent int64
|
||||
|
||||
emit(benchSample{Type: "start", NMsgs: nMsgs, NSubs: nSubs, Payload: payloadBytes, Encrypt: encrypt, Persist: persist})
|
||||
|
||||
t0 := time.Now()
|
||||
done := make(chan struct{})
|
||||
var pubErr atomic.Value
|
||||
go func() {
|
||||
defer close(done)
|
||||
for k := 0; k < nMsgs; k++ {
|
||||
if err := pub.Publish(roomID, payload); err != nil {
|
||||
pubErr.Store(err)
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&sent, 1)
|
||||
if k%256 == 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(60 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
deadline := time.After(120 * time.Second)
|
||||
target := int64(nMsgs) * int64(nSubs)
|
||||
|
||||
sampleLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-deadline:
|
||||
break sampleLoop
|
||||
case <-done:
|
||||
break sampleLoop
|
||||
case <-ticker.C:
|
||||
emit(benchSample{Type: "sample", T: time.Since(t0).Seconds(), Sent: atomic.LoadInt64(&sent), Recv: sumRecv()})
|
||||
}
|
||||
}
|
||||
if v := pubErr.Load(); v != nil {
|
||||
emit(benchSample{Type: "error", Msg: "publish: " + v.(error).Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Final drain: keep sampling until every subscriber has caught up (or we give up).
|
||||
for i := 0; i < 240; i++ {
|
||||
if sumRecv() >= target {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
}
|
||||
emit(benchSample{Type: "sample", T: time.Since(t0).Seconds(), Sent: atomic.LoadInt64(&sent), Recv: sumRecv()})
|
||||
}
|
||||
|
||||
dur := time.Since(t0).Seconds()
|
||||
finalSent := atomic.LoadInt64(&sent)
|
||||
finalRecv := sumRecv()
|
||||
per := make([]int64, nSubs)
|
||||
for i := range counters {
|
||||
per[i] = atomic.LoadInt64(&counters[i])
|
||||
}
|
||||
var pubTps, recvTps int64
|
||||
if dur > 0 {
|
||||
pubTps = int64(float64(finalSent) / dur)
|
||||
recvTps = int64(float64(finalRecv) / dur)
|
||||
}
|
||||
emit(benchSample{Type: "done", T: dur, Sent: finalSent, Recv: finalRecv, PerSub: per, PubTps: pubTps, RecvTps: recvTps, NSubs: nSubs})
|
||||
}
|
||||
|
||||
// handleBench is the SSE endpoint that drives a benchmark from query params:
|
||||
//
|
||||
// GET /api/bench?n_msgs=20000&n_subs=3&payload=128&encrypt=0&persist=0
|
||||
//
|
||||
// Encrypted/persistent runs are capped to a lower message count (the per-message
|
||||
// crypto + JetStream ack make them far slower); the cap is reported in the start
|
||||
// sample so the UI can show it.
|
||||
func (h *Hub) handleBench(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
atoiDef := func(k string, def int) int {
|
||||
if v, err := strconv.Atoi(q.Get(k)); err == nil {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
truthy := func(k string) bool { v := q.Get(k); return v == "1" || v == "true" }
|
||||
|
||||
nMsgs := atoiDef("n_msgs", 20000)
|
||||
nSubs := atoiDef("n_subs", 3)
|
||||
payload := atoiDef("payload", 128)
|
||||
encrypt := truthy("encrypt")
|
||||
persist := truthy("persist")
|
||||
|
||||
if nSubs < 1 {
|
||||
nSubs = 1
|
||||
} else if nSubs > 16 {
|
||||
nSubs = 16
|
||||
}
|
||||
if payload < 1 {
|
||||
payload = 1
|
||||
} else if payload > 8192 {
|
||||
payload = 8192
|
||||
}
|
||||
if nMsgs < 100 {
|
||||
nMsgs = 100
|
||||
}
|
||||
maxMsgs := 200000
|
||||
if encrypt || persist {
|
||||
maxMsgs = 30000 // crypto + JetStream ack are much slower; keep the run bounded
|
||||
}
|
||||
capped := false
|
||||
if nMsgs > maxMsgs {
|
||||
nMsgs, capped = maxMsgs, true
|
||||
}
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeErr(w, http.StatusInternalServerError, "streaming unsupported")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
fmt.Fprintf(w, ": bench start\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
emit := func(s benchSample) {
|
||||
if s.Type == "start" {
|
||||
s.Capped = capped
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n\n", b)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
runBench(r.Context(), emit, nMsgs, nSubs, payload, encrypt, persist)
|
||||
fmt.Fprintf(w, "event: end\ndata: {}\n\n")
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// main: bring up NATS, control plane, and the web server; tear them all down
|
||||
// cleanly on signal.
|
||||
@@ -553,6 +808,7 @@ func main() {
|
||||
mux.HandleFunc("POST /api/publish", hub.handlePublish)
|
||||
mux.HandleFunc("POST /api/kick", hub.handleKick)
|
||||
mux.HandleFunc("GET /api/stream", hub.handleStream)
|
||||
mux.HandleFunc("GET /api/bench", hub.handleBench)
|
||||
webSrv := &http.Server{Addr: webAddr, Handler: mux}
|
||||
go func() {
|
||||
if err := webSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
|
||||
Reference in New Issue
Block a user