Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .grok/skills/improve-docs/references/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Keep it simple. Prefer delete and clarify over decorate.

| Say | Never say |
|-----|-----------|
| **payload size** (256 B, 4 KiB) for the published filter | fixture; five leftover type names as if they were different shapes |
| **payload size** (256 B, 4 KiB) for the published size filter | fixture; five leftover type names as if they were different shapes |
| **pattern** as **1P1C**, **4P4C**, **1P4C**, **4P1C** | SPSC, MPMC on the published filter |
| **data type** only as the catalog id (`size_256`, `size_4096`) | fixture, fixtures, fixtureKey, `dataset.fixtures` |

The published axis is how many bytes each queued item is. Experiment 13 showed that 256 B and 4 KiB tell different stories; 512 B / 1 KiB / 2 KiB do not. Use **payload size** in README, Dashboard copy, and findings. Catalog ids stay `type_id` values (`size_256`, `size_4096`). Older folders may still say `message` / `document` (same lengths). Do not keep `fixture*` as an internal alias. Old `configs.json` may still contain `fixtures`; read it as a fallback, write `data_types`.
Expand All @@ -29,7 +30,7 @@ Do not rank a library on payload size. Every queue in a cell moves the same byte
## Content style

- **One idea per paragraph.** Prefer tables for role/path matrices **on the site / Dashboard**, not by fattening the root README.
- **User terms:** payload size (256 B, 4 KiB), mode (bytes/stream), ops/s, latency, Pareto, baseline.
- **User terms:** payload size (256 B, 4 KiB), pattern (1P1C, 4P4C), ops/s, latency, Pareto, baseline. Do not say SPSC or MPMC on the dashboard filter.
- **Never say “fixture”.** The published sample is a **payload size**. The catalog id is a **data type** (`size_256`, `size_4096`). Say that in docs, Dashboard copy, skills, comments, identifiers, and JSON keys. Do not keep `fixture*` as an internal alias “until later.”
- **Avoid in user copy:** fixture, median size (we do not measure payload size as a result), harness (prefer benchmark runner), unexplained IQR/P95.
- **Honesty line** when ranks appear: within one language; cross-lang directional — prefer **one** place (e.g. Statistics / Method), not a second essay block on README.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Every language writes the same columns (nanoseconds). Domain mapping:
| `TimeEnq` | Enqueue ns |
| `TimeDeq` | Dequeue ns |
| `TimeHandoff` | Handoff ns |
| `Pattern` | `bytes` = **SPSC**, `stream` = **MPMC** (not I/O) |
| `Pattern` | `bytes` = **1P1C**, `4p4c` = **4P4C** (`stream` is leftover 2P2C, not I/O) |
| `CpuTimeNs` | Process CPU time (spin vs block) |

See [Metrics](https://leo-gan.github.io/queue-benchmark/analysis/METRICS/) and [architecture](https://leo-gan.github.io/queue-benchmark/analysis/architecture/).
Expand Down
12 changes: 6 additions & 6 deletions config/benchmark_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ test_data:
description: "4 KiB payload (copy cost starts to matter)"
supports_circular: false

# Pattern axis (CSV StringOrStream). Logged names are serializer ABI leftovers:
# bytes = SPSC, stream = MPMC. Docs and dashboard say SPSC / MPMC, never "stream API".
# Pattern axis (CSV StringOrStream). Logged names are serializer ABI leftovers.
# Docs and dashboard say 1P1C / 2P2C / 1P4C / 4P1C / 4P4C, never "stream API".
io_modes:
- name: bytes
aliases: [string, buffer, spsc]
description: "SPSC: single producer, single consumer"
aliases: [string, buffer, spsc, 1p1c]
description: "1P1C: one producer, one consumer"
- name: stream
aliases: [stream, mpmc]
description: "MPMC: 2 producers, 2 consumers (when the library supports it)"
aliases: [stream, mpmc, 2p2c]
description: "2P2C: two producers, two consumers (when the library supports it)"
- name: 1p4c
aliases: [1p4c]
description: "1 producer, 4 consumers"
Expand Down
6 changes: 3 additions & 3 deletions config/library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Named **run configs** select the measurement matrix:

- `types` (axis W): `type_id` + `type_config` (payload shape)
- `data_type_instance_count` (axis C): items moved per repetition
- `execution.io_modes`: `bytes` = **SPSC**, `stream` = **MPMC** (legacy name; not I/O)
- `execution.io_modes`: `bytes` = **1P1C**, `4p4c` = **4P4C** (CSV leftover `stream` is 2P2C; not I/O)

Published type ids: `size_256` (256 B) · `size_4096` (4 KiB).
Older experiment folders still use `message` / `document` (same lengths).
Expand All @@ -14,8 +14,8 @@ Catalog: `schemas/data_catalog_v2.yaml`.

| File | Purpose |
|------|---------|
| `smoke.yaml` | CI / quick sanity (`size_256`, n=1, SPSC) |
| `default.yaml` | Publication matrix (256 B and 4 KiB × [100, 1000], SPSC+MPMC) |
| `smoke.yaml` | CI / quick sanity (`size_256`, n=1, 1P1C) |
| `default.yaml` | Publication matrix (256 B and 4 KiB × [100, 1000], 1P1C and 4P4C) |

## Usage

Expand Down
7 changes: 4 additions & 3 deletions config/library/default.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Run config: default (publication matrix)
id: default
description: "Two payload sizes (256 B, 4 KiB) × [100, 1000] items; SPSC, 2P2C, 1P4C, 4P1C, 4P4C"
description: "Two payload sizes (256 B, 4 KiB) × [100, 1000] items; 1P1C and 4P4C"
data_model_version: 2

types:
Expand All @@ -16,8 +16,9 @@ compression:

execution:
mode: full
# bytes = SPSC; stream = MPMC (CSV ABI name; not a stream API)
io_modes: [bytes, stream, 1p4c, 4p1c, 4p4c]
# bytes = 1P1C (CSV leftover name). 4p4c = four producers and four consumers.
# 2P2C / 1P4C / 4P1C stay on experiment 3, not the published matrix.
io_modes: [bytes, 4p4c]

budget:
soft_seconds_per_10_serializers: 60
Expand Down
2 changes: 1 addition & 1 deletion config/library/smoke.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Run config: smoke (CI / fast path)
id: smoke
description: "Minimal matrix — 256-byte payload, single instance, SPSC only"
description: "Minimal matrix — 256-byte payload, single instance, 1P1C only"
data_model_version: 2

types:
Expand Down
10 changes: 5 additions & 5 deletions dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@
</div>

<div class="filter-group filter-group-secondary" aria-label="Secondary filters">
<span class="filter-label" title="Concurrency pattern: SPSC (1 producer / 1 consumer) vs MPMC (2 / 2). CSV still logs bytes/stream.">Pattern:</span>
<span class="filter-label" title="How many producers and consumers share the queue. Published cells are 1P1C and 4P4C.">Pattern:</span>
<div class="select-wrapper">
<select id="mode-select" aria-label="Select pattern (SPSC or MPMC)">
<select id="mode-select" aria-label="Select pattern (1P1C or 4P4C)">
<!-- Dynamically populated -->
</select>
</div>
Expand Down Expand Up @@ -156,7 +156,7 @@
</p>
<p class="orientation-gloss section-help">
<strong>Pareto</strong> = not both slower and more CPU-hungry ·
<strong>Pattern</strong> = SPSC vs MPMC ·
<strong>Pattern</strong> = 1P1C or 4P4C ·
<strong>Samples</strong> = which trials after warmup (default drops outliers) ·
<a href="../analysis/COMPARISON_RULES/" id="orientation-rules-link">Comparison rules</a> ·
<a href="../analysis/" id="orientation-method-link">Benchmarks</a> ·
Expand Down Expand Up @@ -302,7 +302,7 @@ <h2 class="chart-title compare-h2">Compare</h2>
<div class="select-wrapper">
<select id="same-data-select" aria-label="Same-language payload size"></select>
</div>
<span class="filter-label" title="SPSC vs MPMC pattern">Pattern</span>
<span class="filter-label" title="How many producers and consumers share the queue">Pattern</span>
<div class="select-wrapper">
<select id="same-mode-select" aria-label="Same-language mode"></select>
</div>
Expand All @@ -317,7 +317,7 @@ <h2 class="chart-title compare-h2">Compare</h2>
<div class="select-wrapper">
<select id="xl-data-select" aria-label="Cross-language payload size"></select>
</div>
<span class="filter-label" title="SPSC vs MPMC pattern">Pattern</span>
<span class="filter-label" title="How many producers and consumers share the queue">Pattern</span>
<div class="select-wrapper">
<select id="xl-mode-select" aria-label="Cross-language mode"></select>
</div>
Expand Down
12 changes: 6 additions & 6 deletions dashboard/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1687,9 +1687,9 @@ function applyFilterPolicyToAllGroups({ refreshSelectors = false } = {}) {
if (modeOptions.includes(wantMode)) {
state.currentMode = wantMode;
} else {
// Prefer SPSC (bytes) when nothing is selected yet. Insertion order in
// the stats file often starts at 1p4c, which hid the usual first view.
const prefer = ['bytes', 'stream', '1p4c', '4p1c', '4p4c'];
// Prefer 1P1C when nothing is selected yet. Insertion order in the
// stats file often starts at 1p4c, which hid the usual first view.
const prefer = ['bytes', '4p4c', 'stream', '1p4c', '4p1c'];
state.currentMode =
prefer.find((m) => modeOptions.includes(m)) || modeOptions[0] || '';
}
Expand Down Expand Up @@ -2545,8 +2545,8 @@ function normalizeMode(mode) {
}

function modeDisplayLabel(norm) {
if (norm === 'bytes' || norm === 'spsc') return 'SPSC';
if (norm === 'stream' || norm === 'mpmc') return 'MPMC (2P2C)';
if (norm === 'bytes' || norm === 'spsc' || norm === '1p1c') return '1P1C';
if (norm === 'stream' || norm === 'mpmc' || norm === '2p2c') return '2P2C';
if (/^\d+p\d+c$/.test(String(norm || ''))) return String(norm).toUpperCase();
return norm || '—';
}
Expand Down Expand Up @@ -2589,7 +2589,7 @@ function summarizeStreamHonesty(groups) {
function updateStreamHonestyChip() {
const chip = document.getElementById('stream-honesty-chip');
if (!chip) return;
// Pattern=stream means MPMC, not stream I/O. Do not show serializer honesty.
// Pattern=stream means 2P2C, not stream I/O. Do not show serializer honesty.
chip.hidden = true;
chip.textContent = '';
chip.removeAttribute('title');
Expand Down
5 changes: 5 additions & 0 deletions dashboard/public/data/available_runs.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"csharp": [
"2026-09-01-162810",
"2026-09-01-153914",
"2026-09-01-143923",
"2026-09-01-131724",
Expand All @@ -17,6 +18,7 @@
"2026-08-31-141025"
],
"rust": [
"2026-09-01-162819",
"2026-09-01-153934",
"2026-09-01-144004",
"2026-09-01-131801",
Expand All @@ -35,6 +37,7 @@
"2026-08-31-141008"
],
"python": [
"2026-09-01-162815",
"2026-09-01-153922",
"2026-09-01-143936",
"2026-09-01-131735",
Expand All @@ -54,6 +57,7 @@
"2026-08-31-140959"
],
"javascript": [
"2026-09-01-162823",
"2026-09-01-153947",
"2026-09-01-144029",
"2026-09-01-131823",
Expand All @@ -72,6 +76,7 @@
"2026-08-31-141008"
],
"c": [
"2026-09-01-162822",
"2026-09-01-153943",
"2026-09-01-144022",
"2026-09-01-131817",
Expand Down
Binary file modified dashboard/public/data/c_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/csharp_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/javascript_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/python_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/rust_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/stats_c_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/stats_csharp_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/stats_javascript_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/stats_python_latest.json.gz
Binary file not shown.
Binary file modified dashboard/public/data/stats_rust_latest.json.gz
Binary file not shown.
16 changes: 8 additions & 8 deletions docs/analysis/BENCHMARK_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ priority, blocking vs spin vs yield, SPSC vs MPMC.

| CSV value | Say this | What actually ran |
|-----------|----------|-------------------|
| `bytes` | **SPSC** | One producer, one consumer |
| `stream` | **MPMC** | Two producers, two consumers |
| `1p4c` / `4p1c` / `4p4c` | Named P×C | Default full matrix + experiment 3 |
| `bytes` | **1P1C** | One producer, one consumer |
| `stream` | **2P2C** | Two producers, two consumers (experiment / old logs) |
| `1p4c` / `4p1c` / `4p4c` | **1P4C** / **4P1C** / **4P4C** | Experiment 3; 4P4C is also on the default matrix |

There is no stream I/O in this suite. In-process queues move already-built
payloads. If a library cannot do MPMC, **skip the cell** — do not fake it
with a mutex around an SPSC structure.
payloads. If a library cannot run with more than one producer or consumer,
**skip the cell** — do not fake it with a mutex around a 1P1C structure.

## What we time

Expand Down Expand Up @@ -75,7 +75,7 @@ Same run modes as [Modes](modes.md): smoke / all-single / full / research.
|------|-----|
| Languages | C, C#, JavaScript, Python, Rust |
| Categories | T and A on the default matrix; P/S/D opt-in (see [categories](queue_categories.md)) |
| Pattern | SPSC, 2P2C, 1P4C, 4P1C, 4P4C (skip if the library cannot) |
| Pattern | 1P1C and 4P4C (skip 4P4C if the library cannot) |
| Payloads | 256 B (`size_256`) and 4 KiB (`size_4096`) |
| Experiments | [01](../experiments/01-spsc-handoff/)–[12](../experiments/12-durable-local/) |

Expand All @@ -94,7 +94,7 @@ Same run modes as [Modes](modes.md): smoke / all-single / full / research.

### Category A

Same SPSC/MPMC cells as T, but the workers are async tasks. Extra tests:
Same 1P1C / 4P4C cells as T, but the workers are async tasks. Extra tests:
experiment 7 (many waiters), 8 (bounded async), 9 (cancel). Do not rank an
A library against a T library.

Expand Down Expand Up @@ -125,7 +125,7 @@ is multi-socket and the run says so.

1. Pick a **language**.
2. Pick a **category** (T or A) in the dashboard Category control.
3. Pick a **pattern** (SPSC or MPMC) and a **payload**.
3. Pick a **pattern** (1P1C or 4P4C) and a **payload size**.
4. Compare families inside that slice.

There is no overall score. A queue that wins T1 can lose T2 or T4.
Expand Down
12 changes: 6 additions & 6 deletions docs/analysis/COMPARISON_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ a chart that looks decisive and is wrong.

## Never

1. **Never publish one “fastest queue” number.** A library that wins SPSC
handoff of a 256-byte ticket can lose at 4 KiB, under 4 producers, or
1. **Never publish one “fastest queue” number.** A library that wins 1P1C
handoff of a 256-byte ticket can lose at 4 KiB, under 4P4C, or
when the queue is bounded. Report by category × pattern × payload.

2. **Never rank across languages.** A C ring and a Python deque are not
Expand All @@ -25,14 +25,14 @@ a chart that looks decisive and is wrong.
concurrency. It does not hand a payload from a producer to a consumer.
It lives under Other, not A.

6. **Never invent MPMC.** If the library cannot run two producers and two
consumers, skip the cell. Do not wrap an SPSC structure in a mutex and
log it as MPMC.
6. **Never invent a multi-worker cell.** If the library cannot run four
producers and four consumers, skip 4P4C. Do not wrap a 1P1C structure
in a mutex and log it as 4P4C.

## Always

- Compare **inside one language and one communication category**.
- Say **SPSC / MPMC** in prose. The CSV still says `bytes` / `stream`.
- Say **1P1C / 4P4C** in prose. The CSV still says `bytes` / `4p4c`.
- A failed fidelity check is an error, not a speed win.
- Warmup index 0 stays in the raw CSV; analysis drops it.

Expand Down
6 changes: 3 additions & 3 deletions docs/analysis/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Do **not** invent `msgs_per_cpu_sec` from wall time.
| `Size` / `SizeGzip` / `SizeZstd` / `median_size_bytes` | Serializer leftovers. Payload bytes are the **data type**, not a result. | **Removed.** New CSVs and stats do not write these. Old logs may still contain `Size`; analysis ignores it. |
| `mean_fidelity` | `FidelityScore` = fraction of items that arrived in order. | **Gate**, not a score. If `< 1`, the row is invalid for ranking. Typical value is `1.0`. |
| `mean_memory_peak_bytes` | Process RSS (`getrusage` / `PeakWorkingSet64` / `process.memoryUsage().rss`). | Provenance. Usually the process, not the queue. Do not run a “most compact” contest on it. |
| `StreamMode` / honesty | Serializer “native vs adapted stream I/O”. There is no stream I/O in this suite. `Pattern=stream` means **MPMC**. | Do not show an honesty column. |
| `StreamMode` / honesty | Serializer “native vs adapted stream I/O”. There is no stream I/O in this suite. `Pattern=stream` means **2P2C**. | Do not show an honesty column. |
| `OpPerSecEnq` / `Deq` / `Handoff` in the CSV | Convenience copies of `1e9 / Time*`. | Analysis recomputes from times. Do not treat CSV ops as a second clock. |

## Identity (not metrics)
Expand All @@ -33,8 +33,8 @@ Do **not** invent `msgs_per_cpu_sec` from wall time.
`DataTypeInstanceCount`, `TypeConfigHash`, `Repetitions`, `RepetitionIndex`,
`RunOrder`, `SchedulePosition`, `NativeKind` (implementation family label).

`Pattern`: `bytes` = SPSC, `stream` = MPMC 2P2C, plus named `1p4c` / `4p1c` /
`4p4c`.
`Pattern`: `bytes` = 1P1C, `stream` = 2P2C (old logs / experiment 3), plus
named `1p4c` / `4p1c` / `4p4c`. The published matrix is 1P1C and 4P4C.

## Derived stats (not CSV columns)

Expand Down
11 changes: 6 additions & 5 deletions docs/analysis/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ serializer-benchmark columns so historical logs keep loading.
| `TimeEnq` | Enqueue ns |
| `TimeDeq` | Dequeue ns |
| `TimeHandoff` | Handoff ns |
| `Pattern` | `bytes` = **SPSC**, `stream` = **MPMC** (not I/O) |
| `Pattern` | `bytes` = **1P1C**, `4p4c` = **4P4C** (`stream` is leftover 2P2C, not I/O) |
| `CpuTimeNs` | Process CPU time (spin vs block) |

## Patterns

- **SPSC** (CSV `bytes`): one producer, one consumer. Every library must implement this.
- **MPMC** (CSV `stream`): two producers, two consumers. Libraries that cannot do MPMC
skip the cell rather than fake it.
- **1P1C** (CSV `bytes`): one producer, one consumer. Every library must implement this.
- **4P4C** (CSV `4p4c`): four producers, four consumers. Libraries that cannot
run that cell skip it rather than fake it.

The word *stream* is a leftover ABI name. The dashboard and docs say SPSC / MPMC.
The word *stream* is a leftover ABI name for 2P2C. The dashboard and docs say
1P1C / 4P4C.
See [Benchmark design](BENCHMARK_DESIGN.md).
2 changes: 1 addition & 1 deletion docs/analysis/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ hard-code repetition counts.

| Mode | Reps | Matrix | Use |
|------|------|--------|-----|
| `smoke` | 2 | `config/library/smoke.yaml` (256 B, n=1, SPSC) | CI, “does it run?” |
| `smoke` | 2 | `config/library/smoke.yaml` (256 B, n=1, 1P1C) | CI, “does it run?” |
| `all-single` | 10 | default matrix | Quick full pass |
| `full` | 100 | default matrix | Publication |
| `research` | 500 | default matrix | High-power stats |
Expand Down
10 changes: 6 additions & 4 deletions docs/analysis/queue_categories.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ These are **properties** that can apply inside T or A:
- bounded vs unbounded (backpressure)
- FIFO vs priority
- blocking vs spin vs yield
- SPSC vs MPMC (workload / pattern)
- 1P1C vs 4P4C (workload / pattern)

## Patterns

| Say | CSV `Pattern` | Work |
|-----|---------------|------|
| SPSC | `bytes` | 1 producer, 1 consumer |
| MPMC | `stream` | 2 producers, 2 consumers |
| 1P1C | `bytes` | 1 producer, 1 consumer |
| 4P4C | `4p4c` | 4 producers, 4 consumers |
| 2P2C | `stream` | 2 producers, 2 consumers (old logs / experiment 3) |
| 1P4C / 4P1C | `1p4c` / `4p1c` | Experiment 3 only |

Logged values `bytes` / `stream` are pattern tags, not I/O.
Logged values `bytes` / `stream` / `4p4c` are pattern tags, not I/O.
2 changes: 1 addition & 1 deletion docs/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
|----------|----------|---------------|---------|-------|
| `mutex-queue` | locked | T (thread) | harness | pthread mutex + ring |
| `lfqueue` | concurrent | T (thread) | lfqueue (vendored) | Lock-free MPMC FIFO |
| `spsc-ring` | spsc | T (thread) | harness | Single-producer ring; MPMC skipped |
| `spsc-ring` | spsc | T (thread) | harness | Single-producer ring; 1P1C only |
| `steal-deque` | work-stealing | T (thread) | harness | Owner-push / steal-from-top |
| `pipe-ipc` | concurrent | P (process) | harness | Opt-in pipe + fork |
| `shared-ring` | spsc | S (shared) | harness | Opt-in mmap + fork ring |
Expand Down
4 changes: 2 additions & 2 deletions docs/experiments/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ One question per folder under `experiments/`. Read [PLAN.md](../../experiments/P

| # | Question |
|---|----------|
| [01-spsc-handoff](../../experiments/01-spsc-handoff/) | SPSC handoff of a small message |
| [02-payload-size](../../experiments/02-payload-size/) | Same SPSC at 4 KiB |
| [01-spsc-handoff](../../experiments/01-spsc-handoff/) | 1P1C handoff of a small message |
| [02-payload-size](../../experiments/02-payload-size/) | Same 1P1C at 4 KiB |
| [03-contention](../../experiments/03-contention/) | 1P4C / 4P1C / 4P4C |
| [04-backpressure](../../experiments/04-backpressure/) | Bounded queue, slow consumer |
| [05-wakeup](../../experiments/05-wakeup/) | Empty-queue wake |
Expand Down
Loading
Loading