diff --git a/docs/cost-model-by-video-minute.md b/docs/cost-model-by-video-minute.md new file mode 100644 index 0000000..bbdf3b5 --- /dev/null +++ b/docs/cost-model-by-video-minute.md @@ -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 | diff --git a/docs/video-clipping-service-implementation-plan.md b/docs/video-clipping-service-implementation-plan.md index 4150ea3..6541231 100644 --- a/docs/video-clipping-service-implementation-plan.md +++ b/docs/video-clipping-service-implementation-plan.md @@ -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 diff --git a/src/test/java/org/example/videoclips/perf/CostModelBenchmarkHarness.java b/src/test/java/org/example/videoclips/perf/CostModelBenchmarkHarness.java new file mode 100644 index 0000000..de5479e --- /dev/null +++ b/src/test/java/org/example/videoclips/perf/CostModelBenchmarkHarness.java @@ -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 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; + } + } +}