rtrm HF Staff commited on
Commit
b73b4ba
·
1 Parent(s): 44d4aeb

first commit

Browse files
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .idea/
2
+
3
+ node_modules
4
+
5
+ # Output
6
+ .output
7
+ .vercel
8
+ .netlify
9
+ .wrangler
10
+ /.svelte-kit
11
+ /build
12
+
13
+ # OS
14
+ .DS_Store
15
+ Thumbs.db
16
+
17
+ # Env
18
+ .env
19
+ .env.*
20
+ !.env.example
21
+ !.env.test
22
+
23
+ # Vite
24
+ vite.config.js.timestamp-*
25
+ vite.config.ts.timestamp-*
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine AS builder
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm ci
5
+ COPY . .
6
+ RUN npm run build
7
+ RUN npm prune --production
8
+
9
+ FROM node:22-alpine
10
+ WORKDIR /app
11
+ COPY --from=builder /app/build build/
12
+ COPY --from=builder /app/node_modules node_modules/
13
+ COPY package.json .
14
+ EXPOSE 3000
15
+ ENV NODE_ENV=production
16
+ CMD [ "node", "build" ]
README.md CHANGED
@@ -1,10 +1,16 @@
1
  ---
2
- title: Cdn Benchmarks
3
- emoji: 🐢
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
+ title: CDN Benchmarks
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: gray
6
  sdk: docker
7
+ app_port: 3000
8
  ---
9
 
10
+ # CDN Benchmarks
11
+
12
+ This project is a web application that shows the performance of different CDNs (Content Delivery Networks) using a simple benchmark test.
13
+ Currently, it supports testing the following CDNs:
14
+ - AWS CloudFront
15
+ - Cloudflare
16
+ - Akamai
eslint.config.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js';
2
+ import { includeIgnoreFile } from '@eslint/compat';
3
+ import svelte from 'eslint-plugin-svelte';
4
+ import globals from 'globals';
5
+ import { fileURLToPath } from 'node:url';
6
+ import ts from 'typescript-eslint';
7
+ import svelteConfig from './svelte.config.js';
8
+
9
+ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
10
+
11
+ export default ts.config(
12
+ includeIgnoreFile(gitignorePath),
13
+ js.configs.recommended,
14
+ ...ts.configs.recommended,
15
+ ...svelte.configs.recommended,
16
+ {
17
+ languageOptions: {
18
+ globals: { ...globals.browser, ...globals.node }
19
+ },
20
+ rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
21
+ // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
22
+ "no-undef": 'off' }
23
+ },
24
+ {
25
+ files: [
26
+ '**/*.svelte',
27
+ '**/*.svelte.ts',
28
+ '**/*.svelte.js'
29
+ ],
30
+ languageOptions: {
31
+ parserOptions: {
32
+ projectService: true,
33
+ extraFileExtensions: ['.svelte'],
34
+ parser: ts.parser,
35
+ svelteConfig
36
+ }
37
+ }
38
+ }
39
+ );
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "huggingfacecloud-probes-result",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "prepare": "svelte-kit sync || echo ''",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "lint": "eslint ."
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/compat": "^1.2.5",
17
+ "@eslint/js": "^9.18.0",
18
+ "@sveltejs/adapter-auto": "^4.0.0",
19
+ "@sveltejs/adapter-node": "^5.2.12",
20
+ "@sveltejs/kit": "^2.16.0",
21
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
22
+ "@tailwindcss/vite": "^4.0.0",
23
+ "eslint": "^9.18.0",
24
+ "eslint-plugin-svelte": "^3.0.0",
25
+ "globals": "^16.0.0",
26
+ "svelte": "^5.0.0",
27
+ "svelte-check": "^4.0.0",
28
+ "tailwindcss": "^4.0.0",
29
+ "typescript": "^5.0.0",
30
+ "typescript-eslint": "^8.20.0",
31
+ "vite": "^6.2.5"
32
+ },
33
+ "dependencies": {
34
+ "@aws-sdk/client-dynamodb": "^3.788.0",
35
+ "@highcharts/map-collection": "^2.3.0",
36
+ "@highcharts/svelte": "^1.1.1",
37
+ "highcharts": "^12.2.0",
38
+ "moment": "^2.30.1"
39
+ }
40
+ }
src/app.css ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ @import 'tailwindcss';
2
+
3
+ html, body {
4
+ background: #0e0f13;
5
+ margin: 0;
6
+ padding: 0;
7
+ font-family: 'Roboto Flex', sans-serif;
8
+ }
src/app.d.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
+ }
12
+
13
+ export {};
src/app.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
src/lib/components/CloudSelect.svelte ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { slide } from 'svelte/transition';
3
+
4
+ export let value: string;
5
+ export let onChange: (value: string) => void = () => {};
6
+
7
+ let isOpen = false;
8
+
9
+ const cloudProviders = [
10
+ {
11
+ id: 'AWS',
12
+ name: 'AWS',
13
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" fill="none">
14
+ <path fill="currentColor" d="M5.51 8.896c0 .198.024.397.074.572.05.149.124.323.199.472.025.05.05.1.05.15 0 .074-.05.149-.125.199l-.398.273c-.074.05-.124.075-.174.075-.074 0-.15-.05-.199-.1q-.15-.15-.224-.298c-.074-.1-.124-.224-.199-.373a2.47 2.47 0 0 1-1.915.895 1.78 1.78 0 0 1-1.294-.472 1.864 1.864 0 0 1 .1-2.587 2.32 2.32 0 0 1 1.592-.523c.224 0 .448.025.696.05q.374.037.747.15v-.473q0-.747-.299-1.045c-.224-.2-.572-.299-1.07-.299-.248 0-.472.025-.696.1-.249.05-.473.124-.696.224-.075.024-.15.05-.25.074-.024 0-.074.025-.099.025-.074 0-.124-.075-.124-.199v-.323c0-.075 0-.15.05-.224s.1-.125.174-.15q.373-.186.82-.298c.324-.075.672-.124.996-.1.77 0 1.343.175 1.691.523.373.348.547.87.547 1.592zm-2.637.994c.223 0 .447-.05.671-.124.249-.075.448-.224.622-.423.1-.124.174-.273.224-.423a2.1 2.1 0 0 0 .075-.572V8.1a6 6 0 0 0-.622-.1 5 5 0 0 0-.597-.05c-.349-.025-.672.075-.97.249-.423.299-.498.87-.2 1.293a.85.85 0 0 0 .797.398m5.198.697a.42.42 0 0 1-.248-.075c-.075-.074-.125-.149-.125-.248l-1.517-5a.9.9 0 0 1-.05-.249c0-.075.05-.15.125-.15h.671c.1 0 .174 0 .249.075s.1.15.124.25l1.095 4.302.995-4.328c.025-.1.05-.174.124-.248a.5.5 0 0 1 .249-.075h.522c.1 0 .174.025.249.074.075.075.1.15.124.25l1.02 4.352 1.144-4.353c.025-.1.075-.174.125-.248a.5.5 0 0 1 .249-.075h.597c.074 0 .149.05.174.124v.05c0 .025 0 .075-.025.1 0 .05-.025.099-.05.174l-1.567 5.024c-.025.1-.075.175-.124.25a.42.42 0 0 1-.25.074h-.546a.42.42 0 0 1-.25-.075c-.074-.074-.099-.174-.123-.249l-.995-4.179-.995 4.18c-.025.099-.05.198-.125.248a.5.5 0 0 1-.249.075zm8.333.174c-.348 0-.671-.05-.994-.124a3.8 3.8 0 0 1-.747-.249.7.7 0 0 1-.199-.174.5.5 0 0 1-.05-.174v-.324c0-.124.05-.199.15-.199.05 0 .074 0 .124.025s.1.05.174.075c.224.1.473.174.722.224.248.05.522.074.77.074.324.025.648-.05.946-.223a.71.71 0 0 0 .348-.622.6.6 0 0 0-.174-.448 2.3 2.3 0 0 0-.647-.348l-.945-.299a2.06 2.06 0 0 1-1.045-.671 1.6 1.6 0 0 1-.323-.946c0-.248.05-.497.174-.721.124-.2.274-.398.473-.522.199-.15.423-.274.671-.349.224-.074.498-.1.772-.1.149 0 .298 0 .422.026.15.024.274.05.423.074.125.025.249.075.373.1.1.025.175.074.274.124.075.05.15.1.199.174.05.075.075.15.05.224v.274c0 .124-.05.199-.15.199a.5.5 0 0 1-.248-.075 3.1 3.1 0 0 0-1.244-.249 1.77 1.77 0 0 0-.87.175.61.61 0 0 0-.3.572.68.68 0 0 0 .2.472c.199.174.448.299.721.348l.92.299c.399.1.747.323.996.622.199.274.298.572.298.92 0 .249-.05.523-.174.746a1.8 1.8 0 0 1-.473.573 2 2 0 0 1-.721.373c-.274.1-.597.124-.896.124"></path><path fill="#F90" fill-rule="evenodd" d="M17.623 13.92c-2.139 1.567-5.223 2.413-7.885 2.413a14.3 14.3 0 0 1-9.652-3.681c-.199-.175-.024-.423.224-.274a19.3 19.3 0 0 0 9.652 2.562 19.2 19.2 0 0 0 7.363-1.517c.348-.15.647.248.298.497" clip-rule="evenodd"></path><path fill="#F90" fill-rule="evenodd" d="M18.519 12.925c-.274-.348-1.791-.174-2.488-.074-.199.024-.248-.15-.05-.299 1.22-.846 3.21-.622 3.458-.323.224.298-.075 2.288-1.194 3.258-.174.15-.348.075-.274-.124.25-.647.821-2.114.548-2.438" clip-rule="evenodd"></path></svg>`
15
+ },
16
+ {
17
+ id: 'GCP',
18
+ name: 'GCP',
19
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" fill="none">
20
+ <path fill="#EA4335" d="M12.027 7.515h.51L13.95 6.07l.057-.623a6.314 6.314 0 0 0-8.916.482l-.085.084a6.6 6.6 0 0 0-1.359 2.604c.17-.056.34-.084.51-.028l2.83-.481s.142-.255.227-.226c1.217-1.416 3.368-1.586 4.812-.368"></path>
21
+ <path fill="#4285F4" d="M15.961 8.618c-.34-1.217-.99-2.32-1.925-3.17l-1.981 2.01a3.6 3.6 0 0 1 1.302 2.859v.368c.99 0 1.783.82 1.783 1.811 0 .963-.792 1.755-1.783 1.783H9.791l-.34.368V16.8l.34.367h3.538a4.627 4.627 0 0 0 4.642-4.642 4.66 4.66 0 0 0-2.01-3.906"></path>
22
+ <path fill="#34A853" d="M6.253 17.138H9.79V14.28H6.253c-.255 0-.51-.056-.736-.17l-.51.17-1.415 1.444-.113.51c.792.594 1.755.933 2.774.905"></path>
23
+ <path fill="#FBBC05" d="M6.252 7.798a4.67 4.67 0 0 0-4.585 4.727c0 1.443.68 2.802 1.811 3.68l2.067-2.095c-.906-.425-1.302-1.472-.878-2.378a1.766 1.766 0 0 1 2.321-.906h.029c.396.17.707.51.877.906L9.96 9.637c-.905-1.16-2.264-1.84-3.708-1.84"></path></svg>`
24
+ }
25
+ ];
26
+
27
+ function handleSelect(providerId: string) {
28
+ value = providerId;
29
+ onChange(providerId);
30
+ isOpen = false;
31
+ }
32
+
33
+ function toggleDropdown() {
34
+ isOpen = !isOpen;
35
+ }
36
+
37
+ function handleClickOutside(event: MouseEvent) {
38
+ const target = event.target as HTMLElement;
39
+ if (!target.closest('.custom-select')) {
40
+ isOpen = false;
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <svelte:window on:click={handleClickOutside}/>
46
+
47
+ <div class="absolute right-5 top-5 w-30 font-roboto custom-select">
48
+ <div
49
+ class="flex items-center gap-2 px-3 py-2 text-white rounded-md cursor-pointer select-none
50
+ bg-[rgba(29,29,33,0.8)] border border-gray-700
51
+ {isOpen ? 'rounded-b-none shadow-sm border-gray-600' : 'hover:border-gray-600'}"
52
+ on:click={toggleDropdown}
53
+ >
54
+ <div class="w-5 h-5 flex items-center justify-center">
55
+ {@html cloudProviders.find(p => p.id === value)?.icon}
56
+ </div>
57
+ <span class="text-white text-sm">
58
+ {cloudProviders.find(p => p.id === value)?.name}
59
+ </span>
60
+ <span class="ml-auto text-white text-xs transition-transform duration-200
61
+ {isOpen ? 'rotate-180' : ''}">
62
+
63
+ </span>
64
+ </div>
65
+
66
+ {#if isOpen}
67
+ <div
68
+ class="absolute w-full bg-[rgba(29,29,33,0.8)] border border-t-0 border-gray-700
69
+ rounded-b-md shadow-lg z-10 overflow-hidden"
70
+ transition:slide={{ duration: 200 }}
71
+ >
72
+ {#each cloudProviders as provider}
73
+ <div
74
+ class="flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors text-white
75
+ {value === provider.id ?
76
+ 'bg-[rgba(29,29,33,0.95)]' :
77
+ 'hover:bg-[rgba(29,29,33,0.95)]'}"
78
+ on:click={() => handleSelect(provider.id)}
79
+ >
80
+ <div class="w-5 h-5 flex items-center justify-center">
81
+ {@html provider.icon}
82
+ </div>
83
+ <span class="text-white text-sm">
84
+ {provider.name}
85
+ </span>
86
+ </div>
87
+ {/each}
88
+ </div>
89
+ {/if}
90
+ </div>
src/lib/components/DetailPanel.svelte ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {onMount} from 'svelte';
3
+ import type {SelectedMapPoint} from "../../routes/+page.server";
4
+ import moment from "moment";
5
+ import type {BenchmarkData} from "$lib/services/dynamodb.service";
6
+ import type {Chart} from "highcharts";
7
+
8
+ export let selectedPoint: SelectedMapPoint;
9
+
10
+ let curlChart: Chart | undefined = undefined;
11
+ let hfChart: Chart | undefined = undefined;
12
+
13
+ function toDataPoint(point: BenchmarkData) {
14
+ return {
15
+ name: point.tool,
16
+ x: point.bench_timestamp / 1000 / 1000,
17
+ y: point.avg_speed,
18
+ custom: {
19
+ benchmark: point
20
+ },
21
+ };
22
+ }
23
+
24
+ async function loadData(): Promise<void> {
25
+ const from = moment().subtract(3, 'days').format('YYYY-MM-DD');
26
+ const data: BenchmarkData[] = await fetch(`/api/benchmarks?from=${from}&cloud=${selectedPoint?.dataCenter?.cloud}&zone=${selectedPoint?.dataCenter?.zone}`)
27
+ .then(response => response.json());
28
+
29
+ const cloudfrontCurlData = data.filter((val) => val.cdn_name === 'CloudFront' && val.tool === 'cURL')
30
+ .map((data) => toDataPoint(data));
31
+
32
+ const cloudflareCurlData = data.filter((val) => val.cdn_name === 'CloudFlare' && val.tool === 'cURL')
33
+ .map((data) => toDataPoint(data));
34
+
35
+ const akamaiCurlData = data.filter((val) => val.cdn_name === 'Akamai' && val.tool === 'cURL')
36
+ .map((data) => toDataPoint(data));
37
+
38
+ curlChart!.update({
39
+ series: [{
40
+ name: 'Cloudfront',
41
+ type: 'spline',
42
+ color: '#f90',
43
+ data: cloudfrontCurlData,
44
+ }, {
45
+ name: 'Cloudflare',
46
+ type: 'spline',
47
+ color: '#f63',
48
+ data: cloudflareCurlData
49
+ }, {
50
+ name: 'Akamai',
51
+ type: 'spline',
52
+ color: '#017ac6',
53
+ data: akamaiCurlData
54
+ }],
55
+ xAxis: {
56
+ type: 'datetime',
57
+ title: {
58
+ text: undefined
59
+ }
60
+ },
61
+ }, true, true, true);
62
+
63
+
64
+ const cloudfrontHfTransferData = data.filter((val) => val.cdn_name === 'CloudFront' && val.tool === 'hf_transfer')
65
+ .map((data) => toDataPoint(data));
66
+
67
+ const cloudflareHfTransferData = data.filter((val) => val.cdn_name === 'CloudFlare' && val.tool === 'hf_transfer')
68
+ .map((data) => toDataPoint(data));
69
+
70
+ const akamaiHfTransferData = data.filter((val) => val.cdn_name === 'Akamai' && val.tool === 'hf_transfer')
71
+ .map((data) => toDataPoint(data));
72
+
73
+ hfChart!.update({
74
+ series: [{
75
+ name: 'Cloudfront',
76
+ type: 'spline',
77
+ color: '#f90',
78
+ data: cloudfrontHfTransferData,
79
+ }, {
80
+ name: 'Cloudflare',
81
+ type: 'spline',
82
+ color: '#f63',
83
+ data: cloudflareHfTransferData
84
+ }, {
85
+ name: 'Akamai',
86
+ type: 'spline',
87
+ color: '#017ac6',
88
+ data: akamaiHfTransferData
89
+ }],
90
+ xAxis: {
91
+ type: 'datetime',
92
+ title: {
93
+ text: undefined
94
+ }
95
+ },
96
+ }, true, true, true);
97
+
98
+ }
99
+
100
+ async function initChart(): Promise<void> {
101
+ const {default: Highcharts} = await import('highcharts');
102
+
103
+ const defaultChartOptions = {
104
+ chart: {
105
+ type: 'spline',
106
+ backgroundColor: 'transparent',
107
+ },
108
+ credits: {
109
+ enabled: false
110
+ },
111
+ title: {
112
+ text: undefined,
113
+ },
114
+ legend: {
115
+ enabled: true,
116
+ itemStyle: {
117
+ color: 'rgb(102, 102, 102)',
118
+ },
119
+ itemHoverStyle: {
120
+ color: '#f90'
121
+ },
122
+ itemHiddenStyle: {
123
+ color: '#555'
124
+ },
125
+ },
126
+ plotOptions: {
127
+ spline: {
128
+ lineWidth: 2.2,
129
+ marker: {
130
+ enabled: false,
131
+ symbol: 'circle',
132
+ radius: 2,
133
+ states: {
134
+ hover: {
135
+ enabled: true,
136
+ },
137
+ },
138
+ },
139
+ states: {
140
+ hover: {
141
+ lineWidth: 2.2,
142
+ },
143
+ },
144
+ }
145
+ },
146
+ yAxis: {
147
+ title: {
148
+ text: 'Throughput (Mbps)'
149
+ },
150
+ gridLineColor: 'rgb(51, 51, 51)',
151
+ min: 0
152
+ },
153
+ series: []
154
+ }
155
+
156
+ curlChart = Highcharts.chart({
157
+ ...defaultChartOptions,
158
+ chart: {
159
+ renderTo: 'curl-chart',
160
+ type: 'spline',
161
+ backgroundColor: 'transparent',
162
+ },
163
+ });
164
+
165
+ hfChart = Highcharts.chart({
166
+ ...defaultChartOptions,
167
+ chart: {
168
+ renderTo: 'hf-chart',
169
+ type: 'spline',
170
+ backgroundColor: 'transparent',
171
+ },
172
+ });
173
+
174
+ await loadData();
175
+ }
176
+
177
+ onMount(() => {
178
+ initChart();
179
+ });
180
+ </script>
181
+
182
+ <h2 class="text-white text-center text-5xl mt-10 uppercase">{selectedPoint?.dataCenter?.zone}</h2>
183
+ <h3 class="text-gray-500 font-light text-center mb-10 mt-5">
184
+ Evolution of the throughput (Mbps) for the last 3 days
185
+ </h3>
186
+
187
+ <h4 class="text-center text-white">cUrl results</h4>
188
+ <div id="curl-chart"></div>
189
+
190
+ <h4 class="text-center text-white mt-10">HF Transfer results</h4>
191
+ <div id="hf-chart"></div>
src/lib/index.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ // place files you want to import through the `$lib` alias in this folder.
src/lib/services/dc.service.ts ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface DataCenterLocation {
2
+ cloud: string;
3
+ zone: string;
4
+ region: string;
5
+ city: string;
6
+ lat: number;
7
+ lon: number;
8
+ country: string;
9
+ }
10
+
11
+ export class DataCenterService {
12
+ private dataCentersLocation: DataCenterLocation[] = [
13
+ {
14
+ cloud : "AWS",
15
+ zone: "us-east-1",
16
+ region: "US East",
17
+ city: "Virginia",
18
+ lat: 38.9940541,
19
+ lon: -77.4524237,
20
+ country: "US",
21
+ },
22
+ {
23
+ cloud : "AWS",
24
+ zone: "us-east-2",
25
+ region: "US East",
26
+ city: "Ohio",
27
+ lat: 39.9611755,
28
+ lon: -82.9987942,
29
+ country: "US",
30
+ },
31
+ {
32
+ cloud : "AWS",
33
+ zone: "us-west-1",
34
+ region: "US West",
35
+ city: "California",
36
+ lat: 37.443680,
37
+ lon: -122.153664,
38
+ country: "US",
39
+ },
40
+ {
41
+ cloud : "AWS",
42
+ zone: "us-west-2",
43
+ region: "US West",
44
+ city: "Oregon",
45
+ lat: 45.9174667,
46
+ lon: -119.2684488,
47
+ country: "US",
48
+ },
49
+ {
50
+ cloud: "AWS",
51
+ zone: "ap-southeast-1",
52
+ region: "Asia Pacific",
53
+ city: "Singapore",
54
+ lat: 1.352083,
55
+ lon: 103.819836,
56
+ country: "SG",
57
+ },
58
+ {
59
+ cloud: "AWS",
60
+ zone: "ap-south-1",
61
+ region: "Asia Pacific",
62
+ city: "Mumbai",
63
+ lat: 19.2425503,
64
+ lon: 72.9667878,
65
+ country: "IN",
66
+ },
67
+ {
68
+ cloud: "AWS",
69
+ zone: "ap-northeast-1",
70
+ region: "Asia Pacific",
71
+ city: "Tokyo",
72
+ lat: 35.617436,
73
+ lon: 139.7459176,
74
+ country: "JP",
75
+ },
76
+ {
77
+ cloud: "AWS",
78
+ zone: "ca-central-1",
79
+ region: "Canada",
80
+ city: "Canada Central",
81
+ lat: 45.5,
82
+ lon: -73.6,
83
+ country: "CA",
84
+ },
85
+ {
86
+ cloud: "AWS",
87
+ zone: "eu-west-3",
88
+ region: "Europe",
89
+ city: "Paris",
90
+ lat: 48.6009709,
91
+ lon: 2.2976644,
92
+ country: "FR",
93
+ },
94
+ {
95
+ cloud: "AWS",
96
+ zone: "eu-north-1",
97
+ region: "Europe",
98
+ city: "Stockholm",
99
+ lat: 59.326242,
100
+ lon: 17.8419717,
101
+ country: "SE",
102
+ },
103
+ {
104
+ cloud: "GCP",
105
+ zone: "us-east1",
106
+ region: "US East",
107
+ city: "South Carolina",
108
+ lat: 33.8568928,
109
+ lon: -80.9450072,
110
+ country: "US",
111
+ },
112
+ {
113
+ cloud: "GCP",
114
+ zone: "us-east4",
115
+ region: "US East",
116
+ city: "Virginia",
117
+ lat: 39.0437578,
118
+ lon: -77.4874419,
119
+ country: "US",
120
+ },
121
+ {
122
+ cloud: "GCP",
123
+ zone: "us-west2",
124
+ region: "US West",
125
+ city: "Los Angeles",
126
+ lat: 34.052235,
127
+ lon: -118.243683,
128
+ country: "US",
129
+ },
130
+ {
131
+ cloud: "GCP",
132
+ zone: "us-central1",
133
+ region: "US Central",
134
+ city: "Iowa",
135
+ lat: 41.5868353,
136
+ lon: -93.6250027,
137
+ country: "US",
138
+ },
139
+ {
140
+ cloud: "GCP",
141
+ zone: "northamerica-northeast1",
142
+ region: "North America",
143
+ city: "Montreal",
144
+ lat: 45.5019,
145
+ lon: -73.5674,
146
+ country: "Canada",
147
+ },
148
+ {
149
+ cloud: "GCP",
150
+ zone: "southamerica-east1",
151
+ region: "South America",
152
+ city: "Sao Paulo",
153
+ lat: -23.550520,
154
+ lon: -46.633308,
155
+ country: "Brazil",
156
+ },
157
+ {
158
+ cloud: "GCP",
159
+ zone: "europe-north1",
160
+ region: "Europe",
161
+ city: "Hamina",
162
+ lat: 60.5719,
163
+ lon: 27.1906,
164
+ country: 'Finland',
165
+ },
166
+ {
167
+ cloud: "GCP",
168
+ zone: "europe-west9",
169
+ region: "Europe",
170
+ city: "Paris",
171
+ lat: 48.8575,
172
+ lon: 2.3514,
173
+ country: 'France',
174
+ },
175
+ {
176
+ cloud: "GCP",
177
+ zone: "asia-east2",
178
+ region: "Asia",
179
+ city: "Hong Kong",
180
+ lat: 22.3193,
181
+ lon: 114.1694,
182
+ country: 'Hong Kong',
183
+ },
184
+ {
185
+ cloud: "GCP",
186
+ zone: "asia-south1",
187
+ region: "Asia",
188
+ city: "Mumbai",
189
+ lat: 19.0760,
190
+ lon: 72.8777,
191
+ country: 'India',
192
+ }
193
+ ]
194
+
195
+ getAllDataCenters(): DataCenterLocation[] {
196
+ return this.dataCentersLocation;
197
+ }
198
+
199
+ getLocation(cloud: string, region: string): DataCenterLocation | undefined {
200
+ return this.dataCentersLocation.find((val) => {
201
+ return val.cloud === cloud && val.region === region;
202
+ });
203
+ }
204
+ }
src/lib/services/dynamodb.service.ts ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {DynamoDBClient, QueryCommand} from "@aws-sdk/client-dynamodb";
2
+ import moment from 'moment'
3
+ import {env} from "$env/dynamic/private";
4
+
5
+ export interface BenchmarkData {
6
+ date: string;
7
+ timestamp: number;
8
+ avg_speed: number;
9
+ bench_timestamp: string;
10
+ cache_hit: boolean;
11
+ cdn_name: string;
12
+ cdn_pop: string;
13
+ cloud: string;
14
+ error: string;
15
+ filesize_mb: number;
16
+ instance_type: string;
17
+ region: string;
18
+ tool: string;
19
+ }
20
+
21
+ class DynamoDBService {
22
+ private client: DynamoDBClient;
23
+ private readonly tableName: string = 'huggingfacecloud-probes';
24
+
25
+ constructor() {
26
+ this.client = new DynamoDBClient({
27
+ region: env.AWS_DEFAULT_REGION,
28
+ credentials: {
29
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
30
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
31
+ }
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Get all benchmarks from the last 3 hours
37
+ */
38
+ async getLastBenchmarks(): Promise<BenchmarkData[]> {
39
+ try {
40
+ const threeHoursAgo = moment().subtract(3, 'hours').unix() * 1000 * 1000 * 1000; // Get nano timestamp
41
+ const today = moment().format('YYYY-MM-DD');
42
+
43
+ const command = new QueryCommand({
44
+ TableName: this.tableName,
45
+ KeyConditionExpression: "#Date = :date AND #Timestamp > :timestamp",
46
+ ExpressionAttributeNames: {
47
+ "#Date": "date",
48
+ "#Timestamp": "timestamp",
49
+ },
50
+ ExpressionAttributeValues: {
51
+ ":date": { S: today },
52
+ ":timestamp": { N: threeHoursAgo.toString() },
53
+ },
54
+ // ProjectionExpression: "#Date, #Timestamp, #Cloud, ",
55
+ });
56
+
57
+ const response = await this.client.send(command);
58
+ if (!response.Items || response.Items.length === 0) {
59
+ console.log("No items found");
60
+ return [];
61
+ }
62
+
63
+ return response.Items.map((item) => ({
64
+ date: item.date.S ?? "",
65
+ timestamp: parseFloat(item.timestamp.N!) ?? 0,
66
+ avg_speed: parseFloat(item.avg_speed.S!) ?? 0,
67
+ bench_timestamp: item.bench_timestamp.N || "",
68
+ cache_hit: item.cache_hit.BOOL || false,
69
+ cdn_name: item.cdn_name.S || "",
70
+ cdn_pop: item.cdn_pop.S || "",
71
+ cloud: item.cloud.S || "",
72
+ error: item.error.S || "",
73
+ filesize_mb: parseFloat(item.filesize_mb.N ?? '0') ?? 0,
74
+ instance_type: item.instance_type.S || "",
75
+ region: item.region.S || "",
76
+ tool: item.tool.S || "",
77
+ }));
78
+ } catch (error) {
79
+ console.error("Error while reading all results", error);
80
+ return [];
81
+ }
82
+ }
83
+
84
+ async getBenchmarks(date: string, cloud: string, region: string): Promise<BenchmarkData[]> {
85
+ try {
86
+ const timestamp = moment(date).unix() * 1000 * 1000 * 1000; // Get nano timestamp
87
+
88
+ const command = new QueryCommand({
89
+ TableName: this.tableName,
90
+ KeyConditionExpression: "#Date = :date AND #Timestamp > :timestamp",
91
+ ExpressionAttributeNames: {
92
+ "#Date": "date",
93
+ "#Timestamp": "timestamp",
94
+ "#Cloud": "cloud",
95
+ "#Region": "region",
96
+ },
97
+ ExpressionAttributeValues: {
98
+ ":date": { S: date },
99
+ ":timestamp": { N: timestamp.toString() },
100
+ ":cloud": { S: cloud },
101
+ ":region": { S: region },
102
+ },
103
+ FilterExpression: "#Cloud = :cloud AND #Region = :region",
104
+ });
105
+
106
+ const response = await this.client.send(command);
107
+ if (!response.Items || response.Items.length === 0) {
108
+ console.log("No items found");
109
+ return [];
110
+ }
111
+
112
+ return response.Items.map((item) => ({
113
+ date: item.date.S ?? "",
114
+ timestamp: parseFloat(item.timestamp.N!) ?? 0,
115
+ avg_speed: parseFloat(item.avg_speed.S!) ?? 0,
116
+ bench_timestamp: item.bench_timestamp.N || "",
117
+ cache_hit: item.cache_hit.BOOL || false,
118
+ cdn_name: item.cdn_name.S || "",
119
+ cdn_pop: item.cdn_pop.S || "",
120
+ cloud: item.cloud.S || "",
121
+ error: item.error.S || "",
122
+ filesize_mb: parseFloat(item.filesize_mb.N ?? '0') ?? 0,
123
+ instance_type: item.instance_type.S || "",
124
+ region: item.region.S || "",
125
+ tool: item.tool.S || "",
126
+ }));
127
+ } catch (error) {
128
+ console.error("Error while reading results", error);
129
+ return [];
130
+ }
131
+ }
132
+ }
133
+
134
+ export const dynamoDBService = new DynamoDBService();
src/routes/+layout.svelte ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svelte:head>
2
+ <link href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,[email protected],100..1000&display=swap" rel="stylesheet">
3
+ </svelte:head>
4
+
5
+ <script lang="ts">
6
+ import '../app.css';
7
+
8
+ let { children } = $props();
9
+ </script>
10
+
11
+ {@render children()}
src/routes/+page.server.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {dynamoDBService} from "$lib/services/dynamodb.service";
2
+ import {type DataCenterLocation, DataCenterService} from "$lib/services/dc.service";
3
+
4
+ import type {SeriesMappointDataOptions, Point} from "highcharts";
5
+
6
+ export interface MapPoint extends SeriesMappointDataOptions {
7
+ dataCenter: DataCenterLocation | undefined,
8
+ }
9
+
10
+ export interface SelectedMapPoint extends Point {
11
+ dataCenter: DataCenterLocation | undefined,
12
+ }
13
+
14
+ export async function load() {
15
+ const dataCenterService = new DataCenterService();
16
+
17
+ const allBenchmarks = await dynamoDBService.getLastBenchmarks();
18
+ const allDataCenters = dataCenterService.getAllDataCenters();
19
+
20
+ // // Add location data to each benchmark
21
+ const mapPoints = allDataCenters.map((dc) => {
22
+ return {
23
+ name: dc.region,
24
+ lat: dc?.lat,
25
+ lon: dc?.lon,
26
+ id: `${dc.cloud}|${dc.region}`,
27
+ dataCenter: dc,
28
+ };
29
+ });
30
+
31
+ return {
32
+ benchmarks: allBenchmarks,
33
+ mapPoints
34
+ };
35
+ }
src/routes/+page.svelte ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {onMount} from 'svelte';
3
+ import {slide} from 'svelte/transition';
4
+ import {quintOut} from 'svelte/easing';
5
+ import CloudSelect from '$lib/components/CloudSelect.svelte';
6
+
7
+ import {type MapChart} from "highcharts";
8
+ import type {MapPoint, SelectedMapPoint} from "./+page.server";
9
+ import type {DataCenterLocation} from "$lib/services/dc.service";
10
+ import DetailPanel from "$lib/components/DetailPanel.svelte";
11
+
12
+ export let data;
13
+
14
+ let isPanelOpen = false;
15
+ let selectedPoint: SelectedMapPoint | undefined = undefined;
16
+ let selectedCloud = 'AWS';
17
+
18
+ let chart: MapChart | undefined = undefined;
19
+
20
+ async function updateData(): Promise<void> {
21
+ if (!chart) {
22
+ return;
23
+ }
24
+
25
+ const datas = data.mapPoints.filter((val) => {
26
+ return val.dataCenter.cloud === selectedCloud;
27
+ });
28
+
29
+ chart.series[1].setData(datas);
30
+ chart.redraw();
31
+ }
32
+
33
+ async function initChart(): Promise<void> {
34
+ const {default: Highcharts} = await import('highcharts');
35
+ window.Highcharts = Highcharts;
36
+ await import('highcharts/modules/map');
37
+
38
+ const topo = await fetch('https://code.highcharts.com/mapdata/custom/world.topo.json')
39
+ .then(response => response.json());
40
+
41
+ chart = Highcharts.mapChart({
42
+ chart: {
43
+ renderTo: 'container',
44
+ map: topo,
45
+ margin: 0,
46
+ height: 900,
47
+ backgroundColor: '#0e0f13',
48
+ },
49
+ credits: {
50
+ enabled: false
51
+ },
52
+ accessibility: {
53
+ enabled: false
54
+ },
55
+ title: {
56
+ text: undefined,
57
+ },
58
+ mapView: {
59
+ projection: {
60
+ name: 'WebMercator',
61
+ },
62
+ center: [-0, 30],
63
+ zoom: 2.5,
64
+ },
65
+ mapNavigation: {
66
+ enabled: true,
67
+ enableButtons: false,
68
+ },
69
+ tooltip: {
70
+ enabled: true,
71
+ backgroundColor: '#161618',
72
+ useHTML: true,
73
+ padding: 15,
74
+ style: {
75
+ color: '#fff',
76
+ },
77
+ distance: 30,
78
+ formatter: function () {
79
+ const dataCenter = (this as Partial<MapPoint>).dataCenter;
80
+ return tooltipDetail(dataCenter!);
81
+ }
82
+ },
83
+ series: [{
84
+ type: 'map',
85
+ name: 'Map',
86
+ nullColor: '#303038',
87
+ borderColor: '#0e0f13',
88
+ showInLegend: false,
89
+ states: {
90
+ inactive: {
91
+ enabled: false
92
+ },
93
+ }
94
+ }, {
95
+ type: 'mappoint',
96
+ name: 'Regions',
97
+ data: [],
98
+ cursor: 'pointer',
99
+ showInLegend: false,
100
+ marker: {
101
+ fillColor: 'rgba(38,188,116,0.7)',
102
+ lineWidth: 18,
103
+ lineColor: 'rgba(38,188,116,0.2)',
104
+ symbol: 'circle',
105
+ radius: 8,
106
+ },
107
+ dataLabels: {
108
+ enabled: false
109
+ },
110
+ point: {
111
+ events: {
112
+ click: function () {
113
+ selectedPoint = this as SelectedMapPoint;
114
+ isPanelOpen = true;
115
+ }
116
+ }
117
+ },
118
+ }]
119
+ });
120
+
121
+ updateData();
122
+ }
123
+
124
+ function closePanel() {
125
+ isPanelOpen = false;
126
+ selectedPoint = undefined;
127
+ }
128
+
129
+ function handleCloudChange() {
130
+ updateData()
131
+ }
132
+
133
+ function tooltipDetail(dataCenter: DataCenterLocation): string {
134
+ const curlAkamaiBench = data.benchmarks.find((val) => {
135
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'Akamai' && val.tool === 'cURL';
136
+ });
137
+ const curlCloudfrontBench = data.benchmarks.find((val) => {
138
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'CloudFront' && val.tool === 'cURL';
139
+ });
140
+ const curlCloudflareBench = data.benchmarks.find((val) => {
141
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'CloudFlare' && val.tool === 'cURL';
142
+ });
143
+ const hfAkamaiBench = data.benchmarks.find((val) => {
144
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'Akamai' && val.tool === 'hf_transfer';
145
+ });
146
+ const hfCloudfrontBench = data.benchmarks.find((val) => {
147
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'CloudFront' && val.tool === 'hf_transfer';
148
+ });
149
+ const hfCloudflareBench = data.benchmarks.find((val) => {
150
+ return val.cloud === dataCenter.cloud && val.region === dataCenter.zone && val.cdn_name === 'CloudFlare' && val.tool === 'hf_transfer';
151
+ });
152
+
153
+ return `
154
+ <div class="w-2xs">
155
+ <div class="p-2 text-center rounded-sm text-black bg-[#26BC74] mb-5">Zone: ${dataCenter.zone}</div>
156
+ <div class="flex pt-2 pb-2">
157
+ <div class="text-zinc-400 flex-1">Location</div>
158
+ <div class="text-white">${dataCenter.city}, ${dataCenter.country}</div>
159
+ </div>
160
+ <div class="flex pt-2 pb-2">
161
+ <div class="text-zinc-400 flex-1">Region</div>
162
+ <div class="text-white">${dataCenter.region}</div>
163
+ </div>
164
+
165
+ <table class="table-auto w-full mt-5 border border-zinc-700">
166
+ <thead class="bg-zinc-700">
167
+ <tr>
168
+ <th class="pt-2 pb-2 text-zinc-400 pl-2">Provider</th>
169
+ <th class="pt-2 pb-2 text-zinc-400 pl-2">Tool</th>
170
+ <th class="text-right pt-2 pb-2 pr-2 text-zinc-400">Bandwidth</th>
171
+ </tr>
172
+ </thead>
173
+ <tbody class="text-gray-400">
174
+ <tr>
175
+ <td class="pl-2">Akamai</td>
176
+ <td class="pl-2">Curl</td>
177
+ <td class="text-right pt-2 pb-2 pr-2">${curlAkamaiBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
178
+ </tr>
179
+ <tr>
180
+ <td class="pl-2">Akamai</td>
181
+ <td class="pl-2">HF Transfer</td>
182
+ <td class="text-right pt-2 pb-2 pr-2">${hfAkamaiBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
183
+ </tr>
184
+ <tr>
185
+ <td class="pl-2">Cloudflare</td>
186
+ <td class="pl-2">Curl</td>
187
+ <td class="text-right pt-2 pb-2 pr-2">${curlCloudflareBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
188
+ </tr>
189
+ <tr>
190
+ <td class="pl-2">Cloudflare</td>
191
+ <td class="pl-2">HF Transfer</td>
192
+ <td class="text-right pt-2 pb-2 pr-2">${hfCloudflareBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
193
+ </tr>
194
+ <tr>
195
+ <td class="pl-2">CloudFront</td>
196
+ <td class="pl-2">Curl</td>
197
+ <td class="text-right pt-2 pb-2 pr-2">${curlCloudfrontBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
198
+ </tr>
199
+ <tr>
200
+ <td class="pl-2">CloudFront</td>
201
+ <td class="pl-2">HF Transfer</td>
202
+ <td class="text-right pt-2 pb-2 pr-2">${hfCloudfrontBench?.avg_speed.toFixed(2) ?? 'N.C'} MB/s</td>
203
+ </tr>
204
+ </tbody>
205
+ </table>
206
+ <div class="text-center text-zinc-400 mt-4 mb-2">
207
+ Click to see more details
208
+ </div>
209
+ </div>
210
+ `;
211
+ }
212
+
213
+ onMount(() => {
214
+ initChart();
215
+ });
216
+ </script>
217
+
218
+ <div class="relative overflow-x-hidden">
219
+ <div class="transition-margin-right duration-300 ease-in-out" class:blur={isPanelOpen}>
220
+ <h1 class="text-white text-center text-5xl mt-10">CDN Benchmark</h1>
221
+ <h3 class="text-gray-500 font-light text-center mb-10 mt-5">
222
+ This map displays the throughput of our CDN in each region.
223
+ </h3>
224
+
225
+ <div class="relative">
226
+ <div id="container" class="border-t border-y-neutral-800"></div>
227
+ <CloudSelect bind:value={selectedCloud} onChange={handleCloudChange}/>
228
+ </div>
229
+ </div>
230
+
231
+ {#if isPanelOpen && selectedPoint}
232
+ <div class="side-panel" transition:slide={{duration: 300, easing: quintOut, axis: 'x'}}>
233
+ <button class="close-button" on:click={closePanel}>✕</button>
234
+ <DetailPanel selectedPoint={selectedPoint} />
235
+ </div>
236
+ {/if}
237
+ </div>
238
+
239
+ <style>
240
+
241
+ .side-panel {
242
+ position: fixed;
243
+ top: 0;
244
+ right: 0;
245
+ width: 70vw;
246
+ height: 100vh;
247
+ background-color: #161618;
248
+ box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
249
+ z-index: 999;
250
+ }
251
+
252
+ .close-button {
253
+ position: absolute;
254
+ top: 20px;
255
+ right: 20px;
256
+ background: none;
257
+ border: none;
258
+ font-size: 1.5rem;
259
+ cursor: pointer;
260
+ color: white;
261
+ padding: 5px 10px;
262
+ border-radius: 5px;
263
+ transition: background-color 0.2s;
264
+ }
265
+
266
+ .close-button:hover {
267
+ background-color: #f0f0f0;
268
+ color: #0e0f13;
269
+ }
270
+
271
+ .blur {
272
+ filter: blur(3px);
273
+ }
274
+ </style>
src/routes/api/benchmarks/+server.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {RequestHandler} from "@sveltejs/kit";
2
+ import {type BenchmarkData, dynamoDBService} from "$lib/services/dynamodb.service";
3
+ import moment from "moment";
4
+
5
+ export const GET: RequestHandler = async ({ url }) => {
6
+ const from = url.searchParams.get('from');
7
+ const cloud = url.searchParams.get('cloud');
8
+ const zone = url.searchParams.get('zone');
9
+
10
+ if (!from || !cloud || !zone) {
11
+ return new Response(JSON.stringify({ error: "Missing parameters" }), {
12
+ status: 400,
13
+ headers: { "Content-Type": "application/json" },
14
+ });
15
+ }
16
+
17
+ let result: BenchmarkData[] = [];
18
+ const start = moment(from);
19
+ const now = moment();
20
+ const diff = now.diff(start, "days");
21
+
22
+ // Get benchmarks for each day
23
+ for (let i = 0; i <= diff; i++) {
24
+ const date = start.clone().add(i, "days").format("YYYY-MM-DD");
25
+ const benchmarks = await dynamoDBService.getBenchmarks(date, cloud, zone);
26
+ result = result.concat(benchmarks);
27
+ }
28
+
29
+ return new Response(JSON.stringify(result), {
30
+ headers: { "Content-Type": "application/json" },
31
+ });
32
+ }
static/favicon.png ADDED
svelte.config.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import adapter from '@sveltejs/adapter-node';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ // Consult https://svelte.dev/docs/kit/integrations
7
+ // for more information about preprocessors
8
+ preprocess: vitePreprocess(),
9
+
10
+ kit: {
11
+ // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
12
+ // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13
+ // See https://svelte.dev/docs/kit/adapters for more information about adapters.
14
+ adapter: adapter()
15
+ }
16
+ };
17
+
18
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "moduleResolution": "bundler"
13
+ }
14
+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15
+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
16
+ //
17
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18
+ // from the referenced tsconfig.json - TypeScript does not merge them in
19
+ }
vite.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import tailwindcss from '@tailwindcss/vite';
2
+ import { sveltekit } from '@sveltejs/kit/vite';
3
+ import { defineConfig } from 'vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [tailwindcss(), sveltekit()]
7
+ });