Add speedtest page
This commit is contained in:
@@ -525,7 +525,6 @@ select:focus {
|
|||||||
color: var(--input_text);
|
color: var(--input_text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--input_hover_background);
|
background: var(--input_hover_background);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active,
|
button:active,
|
||||||
@@ -564,7 +563,7 @@ select.disabled {
|
|||||||
color: var(--input_disabled_text);
|
color: var(--input_disabled_text);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
padding: 4px 5px 4px 5px;
|
padding: 5px 5px 5px 5px;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background: var(--input_background);
|
background: var(--input_background);
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@
|
|||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
<a href="/apps">Apps</a>
|
<a href="/apps">Apps</a>
|
||||||
<a href="/appearance">Theme</a>
|
<a href="/appearance">Theme</a>
|
||||||
|
<a href="/speedtest">Speedtest</a>
|
||||||
<a href="/api">API</a>
|
<a href="/api">API</a>
|
||||||
<a href="/filesystem">Filesystem Guide</a>
|
<a href="/filesystem">Filesystem Guide</a>
|
||||||
<a href="/acknowledgements">Acknowledgements</a>
|
<a href="/acknowledgements">Acknowledgements</a>
|
||||||
|
21
res/template/speedtest.html
Normal file
21
res/template/speedtest.html
Normal 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}}
|
@@ -16,6 +16,7 @@ export default [
|
|||||||
"admin_panel",
|
"admin_panel",
|
||||||
"home_page",
|
"home_page",
|
||||||
"text_upload",
|
"text_upload",
|
||||||
|
"speedtest",
|
||||||
].map((name, index) => ({
|
].map((name, index) => ({
|
||||||
input: `src/${name}.js`,
|
input: `src/${name}.js`,
|
||||||
output: {
|
output: {
|
||||||
|
@@ -96,7 +96,7 @@ const fullscreen = () => {
|
|||||||
class="video"
|
class="video"
|
||||||
on:pause={() => playing = false }
|
on:pause={() => playing = false }
|
||||||
on:play={() => playing = true }
|
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} />
|
<source src={fs_path_url(state.base.path)} type={state.base.file_type} />
|
||||||
</video>
|
</video>
|
||||||
|
8
svelte/src/speedtest.js
Normal file
8
svelte/src/speedtest.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import App from './speedtest/Index.svelte';
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById("page_body"),
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
98
svelte/src/speedtest/Index.svelte
Normal file
98
svelte/src/speedtest/Index.svelte
Normal 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>
|
195
svelte/src/speedtest/Speedtest.svelte
Normal file
195
svelte/src/speedtest/Speedtest.svelte
Normal 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>
|
@@ -30,6 +30,24 @@ export const formatDataVolume = (amt, precision) => {
|
|||||||
}
|
}
|
||||||
return amt + " B"
|
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 second = 1000
|
||||||
const minute = second*60
|
const minute = second*60
|
||||||
|
@@ -3,6 +3,8 @@ export let total = 0
|
|||||||
export let used = 0
|
export let used = 0
|
||||||
export let animation = "ease"
|
export let animation = "ease"
|
||||||
export let speed = 1000
|
export let speed = 1000
|
||||||
|
export let no_animation = false
|
||||||
|
export let style = ""
|
||||||
let percent = 0
|
let percent = 0
|
||||||
$: {
|
$: {
|
||||||
// Avoid division by 0
|
// Avoid division by 0
|
||||||
@@ -19,9 +21,10 @@ $: {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="progress_bar_outer">
|
<div class="progress_bar_outer" style={style}>
|
||||||
<div
|
<div
|
||||||
class="progress_bar_inner"
|
class="progress_bar_inner"
|
||||||
|
class:no_animation
|
||||||
style="width: {percent}%; transition-timing-function: {animation}; transition-duration: {speed}ms;">
|
style="width: {percent}%; transition-timing-function: {animation}; transition-duration: {speed}ms;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +37,7 @@ $: {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 6px 0 12px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
.progress_bar_inner {
|
.progress_bar_inner {
|
||||||
background: var(--highlight_background);
|
background: var(--highlight_background);
|
||||||
@@ -43,4 +46,7 @@ $: {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
}
|
}
|
||||||
|
.no_animation {
|
||||||
|
transition-property: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -152,6 +152,7 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
|||||||
{GET, "abuse" /* */, wc.serveMarkdown("abuse.md", handlerOpts{})},
|
{GET, "abuse" /* */, wc.serveMarkdown("abuse.md", handlerOpts{})},
|
||||||
{GET, "filesystem" /* */, wc.serveMarkdown("filesystem.md", handlerOpts{})},
|
{GET, "filesystem" /* */, wc.serveMarkdown("filesystem.md", handlerOpts{})},
|
||||||
{GET, "apps" /* */, wc.serveTemplate("apps", handlerOpts{})},
|
{GET, "apps" /* */, wc.serveTemplate("apps", handlerOpts{})},
|
||||||
|
{GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})},
|
||||||
|
|
||||||
// User account pages
|
// User account pages
|
||||||
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
|
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
|
||||||
|
Reference in New Issue
Block a user