Add speedtest page

This commit is contained in:
2024-02-19 19:49:34 +01:00
parent a9d685424f
commit 2e5f17d867
11 changed files with 353 additions and 5 deletions

View File

@@ -525,7 +525,6 @@ select:focus {
color: var(--input_text);
text-decoration: none;
background: var(--input_hover_background);
}
button:active,
@@ -564,7 +563,7 @@ select.disabled {
color: var(--input_disabled_text);
box-shadow: none;
transition: none;
padding: 4px 5px 4px 5px;
padding: 5px 5px 5px 5px;
cursor: not-allowed;
background: var(--input_background);
}

View File

@@ -27,6 +27,7 @@
<a href="/about">About</a>
<a href="/apps">Apps</a>
<a href="/appearance">Theme</a>
<a href="/speedtest">Speedtest</a>
<a href="/api">API</a>
<a href="/filesystem">Filesystem Guide</a>
<a href="/acknowledgements">Acknowledgements</a>

View File

@@ -0,0 +1,21 @@
{{define "speedtest"}}
<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Speedtest"}}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<link rel='stylesheet' href='/res/svelte/speedtest.css?v{{cacheID}}'>
<script defer src='/res/svelte/speedtest.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -16,6 +16,7 @@ export default [
"admin_panel",
"home_page",
"text_upload",
"speedtest",
].map((name, index) => ({
input: `src/${name}.js`,
output: {

View File

@@ -96,7 +96,7 @@ const fullscreen = () => {
class="video"
on:pause={() => playing = false }
on:play={() => playing = true }
on:ended={() => dispatch("next", {})}
on:ended={() => dispatch("open_sibling", 1)}
>
<source src={fs_path_url(state.base.path)} type={state.base.file_type} />
</video>

8
svelte/src/speedtest.js Normal file
View File

@@ -0,0 +1,8 @@
import App from './speedtest/Index.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -0,0 +1,98 @@
<script>
import Footer from "../layout/Footer.svelte";
import Speedtest from "./Speedtest.svelte";
</script>
<header>
<h1>Pixeldrain Speedtest</h1>
</header>
<div class="page_content">
<Speedtest/>
<section>
<h2>How does this work?</h2>
<p>
The speedtest measures the maximum download speed from pixeldrain's
servers to your computer. This speed is not affected by the daily
download limit for free users.
</p>
<h2>What do the numbers mean?</h2>
<p>
The first number is the download speed in bytes per second. This is
useful for estimating how long a file download will take.
</p>
<ul>
<li>1 kB = 1000 bytes</li>
<li>1 MB = 1000 kB</li>
<li>1 GB = 1000 MB</li>
<li>etc..</li>
</ul>
<p>
The second number shows the download speed in bits. This is usually
the speed your ISP advertises to you. This number is equal to the
first number but multiplied by 8, because there are eight bits in a
byte.
</p>
<p>
The third number shows the latency to the pixeldrain servers. This
is dependent on how far you are removed from the closest pixeldrain
server. The lower the latency the faster your downloads will be,
generally. The number shows request latency and not ping latency.
HTTP requests have some overhead which means this latency number
shows multiple round trips instead of one.
</p>
<p>
The last number shows how much data the speedtest was able to
transfer in duration of the test. The standard test is five seconds
and the long test is 10 seconds. The long test is better
representative of the real download speed because the speed needs to
ramp up at the beginning which also takes time.
</p>
<h2>Why is the speed different from other speed tests?</h2>
<p>
Most speed tests have servers in datacenters which are located very
close to your home. They are also connected directly to your ISP
which means that the ISP does not have to pay for the bandwidth
because it stays within their network.
</p>
<p>
Pixeldrain does not have this luxury. Because our budget is very
small we are only able to afford the cheapest bandwidth available.
This means that the data has to travel further and is more likely to
be throttled.
</p>
<h2>How do I read these results?</h2>
<p>
If the speed is a lot slower than your usual downloads it can mean
two things.
</p>
<ol>
<li>
Your ISP is limiting the connection speed to pixeldrain's
servers
</li>
<li>
The pixeldrain servers are overloaded
</li>
</ol>
<p>
Number two is usually not the case, when that happens I will
<a href="https://twitter.com/Fornax96">tweet about it</a>.
Unfortunately there is nothing I can do about the first scenario. I
might be able to get more servers and better bandwidth if the site
gets many more subscribers, but for now that's out of reach.
</p>
</section>
</div>
<Footer/>
<style>
.page_content {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,195 @@
<script>
import { onMount } from "svelte";
import Button from "../layout/Button.svelte";
import { formatDataVolume, formatDataVolumeBits } from "../util/Formatting.svelte";
import ProgressBar from "../util/ProgressBar.svelte";
import { copy_text } from "../util/Util.svelte";
let running = false
let data_received = 0
let update_interval = 100
let test_duration = 0
let current_duration = 0
let latency = 0
const start = async (dur = 6000) => {
if (running) {
return
}
running = true
test_duration = dur
data_received = 0
let latency_start = Date.now()
// Start a request for 10 GB of random data. We omit credentials so the
// server does fetch the API key from the database which increases latency
let req = await fetch(
window.api_endpoint+"/misc/speedtest?limit="+10e9,
{credentials: "omit"},
)
// Measure request latency
latency = Date.now() - latency_start
let reader = req.body.getReader();
measure_speed(reader, update_interval, test_duration)
// Read from the connection, add the received data to the total
while(true) {
const {done, value} = await reader.read();
if (done) {
break;
}
data_received += value.length;
}
running = false
}
// Average speed for the whole test
let speed = 0
let result_link = ""
const measure_speed = (reader, update_interval, test_duration) => {
// We measure the transfer speed for 1/3 the duration of the test, after
// that we start overwriting the lowest speed values with the highest speed
// values to account for slow start and jitter. At the end of the test the
// average speed in this array is the final result.
let hist = new Array((test_duration/3)/update_interval)
let idx = 0
let previous_transferred = 0
let start = Date.now()
console.debug("History length", hist.length)
let measure = async () => {
let current_speed = data_received - previous_transferred
previous_transferred = data_received
// If the current measurement is higher than the last measurement we
// save it in the speed history array
if (hist[idx%hist.length] === undefined || current_speed > hist[idx%hist.length]) {
hist[idx%hist.length] = current_speed
}
idx++
// Calculate the average of the speed measurements
let sum = hist.reduce((acc, val) => {
if (val !== undefined) {
acc.sum += val
acc.count++
}
return acc
}, {sum: 0, count: 0})
speed = (sum.sum/sum.count)*(1000/update_interval)
// Only used for the progress bar
current_duration = Date.now() - start
if (idx < test_duration/update_interval) {
// We have to manually calculate and subtract drift, because in my
// tests with setInterval the clock would drift like 200ms in a
// single test which significantly impacts results
setTimeout(measure, update_interval - (current_duration-(idx*update_interval)))
} else {
console.debug("Done! Test ran for", current_duration, )
current_duration = 0
await reader.cancel()
history.replaceState(
undefined,
undefined,
"#s="+speed+"&l="+latency+"&t="+data_received,
)
result_link = window.location.href
}
}
setTimeout(measure, update_interval)
}
onMount(() => {
if (window.location.hash[0] === "#") {
var hash = window.location.hash.replace("#", "");
let result = hash.replace("#", "").split('&').reduce((res, item) => {
let parts = item.split('=')
let n = Number(parts[1])
if (n !== NaN) {
res[parts[0]] = n
}
return res;
}, {});
if (result.s && result.l && result.t) {
speed = result.s
latency = result.l
data_received = result.t
result_link = window.location.href
}
}
})
</script>
<section class="highlight_border">
<div style="text-align: center">
<Button icon="speed" label="Start test" click={() => start(6000)} disabled={running} highlight={!running}/>
<Button icon="speed" label="Long test" click={() => start(12000)} disabled={running}/>
<Button
highlight_on_click
disabled={result_link === ""}
icon="content_copy"
label="Copy test result"
click={e => copy_text(result_link)}
/>
</div>
<ProgressBar animation="linear" speed={update_interval} total={test_duration} used={current_duration}/>
<div class="speed_stats">
<div class="highlight_shaded">{formatDataVolume(speed, 4)}/s</div>
<div class="highlight_shaded">{formatDataVolumeBits(speed, 4)}ps</div>
<div class="highlight_shaded">Latency {latency}ms</div>
<div class="highlight_shaded">Received {formatDataVolume(data_received, 3)}</div>
</div>
<!-- Progress bar starts at log10(3) becasue the we want the lowest speed shown to be 1 kB/s -->
<ProgressBar animation="linear" speed={update_interval} used={Math.log10(speed*8)-5} total={5}/>
<div class="speed_grid">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="speed_grid">
<div>100 kb</div>
<div>1 Mb</div>
<div>10 Mb</div>
<div>100 Mb</div>
<div>1 Gb</div>
<div>10 Gb</div>
</div>
</section>
<style>
.speed_stats {
display: flex;
flex-wrap: wrap;
gap: 4px;
font-size: 1.5em;
}
.speed_stats > * {
flex: 1 0 9em;
}
.speed_grid {
display: flex;
justify-content: space-between;
line-height: 1em;
}
.speed_grid > * {
flex: 0 0 auto;
}
</style>

View File

@@ -30,6 +30,24 @@ export const formatDataVolume = (amt, precision) => {
}
return amt + " B"
}
export const formatDataVolumeBits = (amt, precision) => {
amt = amt*8
if (precision < 3) { precision = 3; }
if (amt >= 1e18-1e15) {
return (amt/1e18).toPrecision(precision) + " Eb";
}else if (amt >= 1e15-1e12) {
return (amt/1e15).toPrecision(precision) + " Pb";
}else if (amt >= 1e12-1e9) {
return (amt/1e12).toPrecision(precision) + " Tb";
} else if (amt >= 1e9-1e6) {
return (amt/1e9).toPrecision(precision) + " Gb";
} else if (amt >= 1e6-1e3) {
return (amt/1e6).toPrecision(precision) + " Mb";
} else if (amt >= 1e3-1) {
return (amt/1e3).toPrecision(precision) + " kb";
}
return amt + " b"
}
const second = 1000
const minute = second*60

View File

@@ -3,6 +3,8 @@ export let total = 0
export let used = 0
export let animation = "ease"
export let speed = 1000
export let no_animation = false
export let style = ""
let percent = 0
$: {
// Avoid division by 0
@@ -19,9 +21,10 @@ $: {
}
</script>
<div class="progress_bar_outer">
<div class="progress_bar_outer" style={style}>
<div
class="progress_bar_inner"
class:no_animation
style="width: {percent}%; transition-timing-function: {animation}; transition-duration: {speed}ms;">
</div>
</div>
@@ -34,7 +37,7 @@ $: {
height: 6px;
border-radius: 6px;
overflow: hidden;
margin: 6px 0 12px 0;
margin: 6px 0;
}
.progress_bar_inner {
background: var(--highlight_background);
@@ -43,4 +46,7 @@ $: {
border-radius: 6px;
transition-property: width;
}
.no_animation {
transition-property: none;
}
</style>

View File

@@ -152,6 +152,7 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
{GET, "abuse" /* */, wc.serveMarkdown("abuse.md", handlerOpts{})},
{GET, "filesystem" /* */, wc.serveMarkdown("filesystem.md", handlerOpts{})},
{GET, "apps" /* */, wc.serveTemplate("apps", handlerOpts{})},
{GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})},
// User account pages
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},