Spaces:
Running
Running
first commit
Browse files- .gitignore +25 -0
- .npmrc +1 -0
- Dockerfile +16 -0
- README.md +12 -6
- eslint.config.js +39 -0
- package-lock.json +0 -0
- package.json +40 -0
- src/app.css +8 -0
- src/app.d.ts +13 -0
- src/app.html +12 -0
- src/lib/components/CloudSelect.svelte +90 -0
- src/lib/components/DetailPanel.svelte +191 -0
- src/lib/index.ts +1 -0
- src/lib/services/dc.service.ts +204 -0
- src/lib/services/dynamodb.service.ts +134 -0
- src/routes/+layout.svelte +11 -0
- src/routes/+page.server.ts +35 -0
- src/routes/+page.svelte +274 -0
- src/routes/api/benchmarks/+server.ts +32 -0
- static/favicon.png +0 -0
- svelte.config.js +18 -0
- tsconfig.json +19 -0
- vite.config.ts +7 -0
.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:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
-
|
8 |
---
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
});
|