I added a reproducible cost-model harness in src/test/java/org/example/videoclips/perf/CostModelBenchmarkHarness.java:1 that turns the checked-in FFmpeg and object-storage benchmark baselines into per-source-minute estimates

under explicit planning assumptions. The checked-in companion doc is docs/cost-model-by-video-minute.md:1, and it now includes the modeled baseline table:

  - exact-veryfast-720p: 0.000754 USD / source min
  - exact-medium-720p: 0.000816 USD / source min
  - exact-veryfast-720p-slow-storage: 0.000802 USD / source min
This commit is contained in:
JSLMPR 2026-07-09 19:04:23 +02:00
parent 632959104b
commit 1b9246bafe
3 changed files with 178 additions and 1 deletions

View File

@ -0,0 +1,44 @@
# Cost Model By Video Minute
This document captures the reproducible planning baseline for performance plan item `Cost model by video minute`.
What this model is:
- A benchmark-backed planning model for cost per source-video minute.
- Derived from the checked-in FFmpeg runtime baseline and object-storage bandwidth baseline.
- Based on explicit example cost assumptions that are intended to be replaced with current vendor-specific prices during rollout.
What this model is not:
- It is not live cloud pricing.
- It is not a production invoice forecast.
- It does not include cross-region egress, observability platform costs, database costs, or support overhead.
How to rerun:
```bash
mvn -q -Dtest=CostModelBenchmarkHarness test
```
Generated artifact:
- `target/benchmarks/cost-model-by-video-minute.md`
Interpretation notes:
- `compute` reflects FFmpeg worker time from the preset benchmark.
- `transfer` reflects worker time spent waiting on object-storage throughput assumptions.
- `retention` scales with source and output bitrate assumptions plus retention days.
- `requests` captures one source GET plus clip PUT activity and remains a minor component in the modeled scenarios.
Operational note:
- Replace the example hourly and storage rates with current prices from the target cloud before using this model for budgeting or margin decisions.
Modeled baseline on this workspace:
| Scenario | Compute USD / source min | Transfer USD / source min | Retention USD / source min | Request USD / source min | Total USD / source min |
| --- | ---: | ---: | ---: | ---: | ---: |
| `exact-veryfast-720p` | 0.000189 | 0.000048 | 0.000472 | 0.000045 | 0.000754 |
| `exact-medium-720p` | 0.000251 | 0.000048 | 0.000472 | 0.000045 | 0.000816 |
| `exact-veryfast-720p-slow-storage` | 0.000189 | 0.000096 | 0.000472 | 0.000045 | 0.000802 |

View File

@ -715,7 +715,7 @@ Keep the domain independent from Spring framework details. Adapters implement st
3. [x] Object-storage bandwidth tests.
4. [x] Queue visibility-timeout tuning.
5. [x] Autoscaling policies.
6. [ ] Cost model by video minute.
6. [x] Cost model by video minute.
### Operational Readiness

View File

@ -0,0 +1,133 @@
package org.example.videoclips.perf;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
class CostModelBenchmarkHarness {
@Test
void generateCostModelByVideoMinuteReport() throws IOException {
Path reportDir = Path.of("target/benchmarks");
Files.createDirectories(reportDir);
CostAssumptions assumptions = new CostAssumptions(
0.096,
0.023,
0.005,
0.0004,
8.0,
4.0,
7,
7
);
List<Scenario> scenarios = List.of(
new Scenario("exact-veryfast-720p", 16.0, 1.89261, 50.0),
new Scenario("exact-medium-720p", 16.0, 2.50786, 50.0),
new Scenario("exact-veryfast-720p-slow-storage", 16.0, 1.89261, 25.0)
);
StringBuilder builder = new StringBuilder();
builder.append("# Cost Model By Video Minute\n\n");
builder.append("Methodology:\n");
builder.append("- Compute runtime ratio comes from the checked-in FFmpeg preset benchmark.\n");
builder.append("- Storage transfer time uses the checked-in object-storage bandwidth baseline.\n");
builder.append("- Costs below use explicit planning assumptions, not live cloud pricing.\n");
builder.append("- Replace the hourly and storage rates with current vendor-specific numbers before production budgeting.\n\n");
builder.append("Assumptions:\n");
builder.append("- Worker pod cost: `$").append(format(assumptions.workerPodUsdPerHour())).append("` per hour\n");
builder.append("- Object storage: `$").append(format(assumptions.objectStorageUsdPerGiBMonth())).append("` per GiB-month\n");
builder.append("- PUT requests: `$").append(format(assumptions.putUsdPerThousandRequests())).append("` per 1,000 requests\n");
builder.append("- GET requests: `$").append(format(assumptions.getUsdPerThousandRequests())).append("` per 1,000 requests\n");
builder.append("- Source bitrate: `").append(format(assumptions.sourceBitrateMbps())).append(" Mbps`\n");
builder.append("- Output bitrate: `").append(format(assumptions.outputBitrateMbps())).append(" Mbps`\n");
builder.append("- Source retention: `").append(assumptions.sourceRetentionDays()).append("` days\n");
builder.append("- Clip retention: `").append(assumptions.clipRetentionDays()).append("` days\n\n");
builder.append("| Scenario | Compute USD / source min | Transfer USD / source min | Retention USD / source min | Request USD / source min | Total USD / source min |\n");
builder.append("| --- | ---: | ---: | ---: | ---: | ---: |\n");
for (Scenario scenario : scenarios) {
ScenarioCost cost = calculateScenarioCost(scenario, assumptions);
builder.append("| ")
.append(scenario.name()).append(" | ")
.append(format(cost.computeUsdPerSourceMinute())).append(" | ")
.append(format(cost.transferUsdPerSourceMinute())).append(" | ")
.append(format(cost.retentionUsdPerSourceMinute())).append(" | ")
.append(format(cost.requestUsdPerSourceMinute())).append(" | ")
.append(format(cost.totalUsdPerSourceMinute())).append(" |\n");
}
builder.append("\nFormulas:\n");
builder.append("- `compute = worker_seconds_per_source_minute / 3600 * worker_pod_usd_per_hour`\n");
builder.append("- `transfer = staging_seconds_per_source_minute / 3600 * worker_pod_usd_per_hour`\n");
builder.append("- `retention = retained_gib_per_source_minute * usd_per_gib_month * retention_days / 30`\n");
builder.append("- `requests = (1 GET + 1 PUT + clip_count PUTs) * request_rate`\n");
Path reportPath = reportDir.resolve("cost-model-by-video-minute.md");
Files.writeString(reportPath, builder.toString());
System.out.println("Cost model report written to " + reportPath.toAbsolutePath());
}
private ScenarioCost calculateScenarioCost(Scenario scenario, CostAssumptions assumptions) {
double workerSecondsPerSourceMinute = scenario.ffmpegWallSecondsForSample() / scenario.sampleDurationSeconds() * 60.0;
double sourceMiBPerMinute = assumptions.sourceBitrateMbps() * 60.0 / 8.0;
double outputMiBPerMinute = assumptions.outputBitrateMbps() * 60.0 / 8.0;
double transferSecondsPerSourceMinute = (sourceMiBPerMinute + outputMiBPerMinute) / scenario.storageBandwidthMiBPerSecond();
double computeUsdPerSourceMinute = workerSecondsPerSourceMinute / 3600.0 * assumptions.workerPodUsdPerHour();
double transferUsdPerSourceMinute = transferSecondsPerSourceMinute / 3600.0 * assumptions.workerPodUsdPerHour();
double retainedGiBPerSourceMinute = ((sourceMiBPerMinute * assumptions.sourceRetentionDays())
+ (outputMiBPerMinute * assumptions.clipRetentionDays()))
/ 1024.0;
double retentionUsdPerSourceMinute = retainedGiBPerSourceMinute
* assumptions.objectStorageUsdPerGiBMonth()
/ 30.0;
int clipCountPerSourceMinute = (int) Math.ceil(60.0 / 8.0);
double requestUsdPerSourceMinute = assumptions.getUsdPerThousandRequests() / 1000.0
+ ((1.0 + clipCountPerSourceMinute) * assumptions.putUsdPerThousandRequests() / 1000.0);
return new ScenarioCost(
computeUsdPerSourceMinute,
transferUsdPerSourceMinute,
retentionUsdPerSourceMinute,
requestUsdPerSourceMinute
);
}
private String format(double value) {
return String.format(Locale.ROOT, "%.6f", value);
}
private record CostAssumptions(
double workerPodUsdPerHour,
double objectStorageUsdPerGiBMonth,
double putUsdPerThousandRequests,
double getUsdPerThousandRequests,
double sourceBitrateMbps,
double outputBitrateMbps,
int sourceRetentionDays,
int clipRetentionDays
) {
}
private record Scenario(String name, double sampleDurationSeconds, double ffmpegWallSecondsForSample, double storageBandwidthMiBPerSecond) {
}
private record ScenarioCost(
double computeUsdPerSourceMinute,
double transferUsdPerSourceMinute,
double retentionUsdPerSourceMinute,
double requestUsdPerSourceMinute
) {
private double totalUsdPerSourceMinute() {
return computeUsdPerSourceMinute + transferUsdPerSourceMinute + retentionUsdPerSourceMinute + requestUsdPerSourceMinute;
}
}
}