Repository Wiki
jason5ng32/MyIP

下载、上传与延迟测速

测速功能使用 @cloudflare/speedtest 引擎,在浏览器中组织下载、上传、延迟和抖动测量。实现先执行固定的小规模预览,再切换到使用启动时配置的独立正式测试,并将成功结果发送给应用内事件消费者。

目的与范围

本页覆盖测速会话的构造、预览与正式测试切换、实时读数、进度、暂停与恢复、结果评分和完成事件。核心依据是 speedtest-session.js 与 SpeedTest.vue 的已读取实现。

IP 历史、Globalping、成就和报告仅说明本功能向它们提供数据的边界,不展开其内部实现。当前提供的目录上下文没有这些相邻页面的准确路径,因此不构造未验证的 Wiki 链接。SDK 内部请求调度、测速服务器选择、重试策略,以及未读取的界面默认配置不属于本页已核验内容。

概述

测速过程有两个相互独立的层次:

  • 会话层:createSpeedTestSession 接受引擎构造器和测量配置,管理预览、正式测试以及引擎回调生命周期。
  • 界面层:engineMethods、setupTestEngine 和 speedTestController 管理 Vue 状态、图表、成功与失败判定、评分和应用事件。

预览只测两次延迟和一次 100,000 字节下载,不包含上传。正式测试重新创建引擎,按延迟、下载、上传顺序提交用户配置。预览的测量结果不会作为正式引擎的原始样本继续累积;不过界面会保留已经显示的预览读数,直到对应正式指标产生有效样本。

这种分离同时满足两个目的:较早显示可用反馈,以及让正式测试采用独立样本。它也意味着读数暂时可来自不同阶段,不应将测试中的某一帧视为最终报告。

架构与数据边界

Loading diagram...

Sources: speedtest-session.js、SpeedTest.vue

engineMethods.reset() 使用 markRaw 包装新会话;状态重置则由控制器负责。这使“建立引擎”和“清理旧展示状态”成为两个明确职责,而不是在构造引擎时隐式修改所有界面数据。

连接信息是另一条辅助数据链:getIPFromSpeedTest() 请求 Cloudflare trace,解析 ip、colo、loc,验证 IP,随后动态加载机房信息并转换国家名称。失败会记录日志并返回 null。loadConnection(currentRun) 只在尚无连接 IP 时发起查询,并仅在轮次仍匹配时写入状态和调用 store.updateAllIPs。该入口把测速获得的 IP 提供给其他功能,但不证明其他功能的存储方式。

依据:SpeedTest.vue、SpeedTest.vue。

会话生命周期与核心控制流

1. 启动时保存正式测试配置

会话创建时立即把 packages 的值复制到 measurements。因此在预览期间修改配置对象,不会改变已经构造的正式测量列表。

javascript
1 const measurements = [ 2 { type: 'latency', numPackets: packages.latency.count }, 3 { type: 'download', bytes: packages.download.bytes, count: packages.download.count }, 4 { type: 'upload', bytes: packages.upload.bytes, count: packages.upload.count }, 5 ];

Source: speedtest-session.js

这段代码同时定义了正式阶段的顺序,以及界面配置到 SDK 字段的映射:延迟使用 numPackets,吞吐测试使用 bytes 与 count。封装没有执行输入范围校验,因此不能把它当成已经验证参数的公共服务端 API。

2. 预览与正式引擎隔离

所有引擎均使用 autoStart: false。预览额外设置最小带宽请求时间为零、跳过下载最小时长限制、关闭下载与上传负载延迟测量,并令 logAimApiUrl 为 null。正式引擎只显式获得 autoStart 和 measurements,其他行为交给 SDK 默认值;这些默认值未在已读仓库代码中展开。

Loading diagram...

Source: speedtest-session.js

切换之前的最后一次 onResultsChange() 很重要:SDK 排队的最后样本先被读取,之后才替换当前结果对象。正常预览结束不会触发界面的最终完成处理;只有正式结束,或者已失败的预览结束,才将结果交给会话的 onFinish。

3. 暂停、恢复与重新测量

speedTestController 根据状态分支处理操作:

当前状态已核验行为
running调用 testEngine.pause(),设置 paused 并返回
paused调用 testEngine.play() 并返回,不重建会话
其他状态增加 runId,销毁仍存在的旧会话,重置并销毁图表,重置指标和评分,构造新会话并注册回调

新一轮把基本指标置零、负载延迟与评分置为 '-',进度置零、阶段置为 probe。本次读取到控制器的 setupTestEngine() 为止,后续首次启动及组件卸载的具体调用顺序未核验,不作推断。

会话的 play() 在已销毁或已经运行时直接返回;pause() 会暂停当前引擎并通知界面;destroy() 标记销毁、解绑回调并暂停当前引擎,没有向界面再次发送暂停状态。

依据:SpeedTest.vue、speedtest-session.js。

实时读数、进度与最终值

样本门控而非直接读取零值

getSpeedTestSampleCounts(raw) 统计延迟 timings 数量,并累加下载、上传各 bucket 的 timings.length。getSpeedTestLiveValues(results) 只有在存在对应样本时才调用 getter;抖动至少需要两个延迟样本。

javascript
1 const metrics = [ 2 ['downloadSpeed', 'getDownloadBandwidth', counts.download, 1e6], 3 ['uploadSpeed', 'getUploadBandwidth', counts.upload, 1e6], 4 ['latency', 'getUnloadedLatency', counts.latency, 1], 5 ['jitter', 'getUnloadedJitter', counts.latency >= 2, 1], 6 ]; 7 return Object.fromEntries(metrics.flatMap(([key, getter, hasSamples, divisor]) => { 8 if (!hasSamples) return []; 9 const value = results[getter](); 10 return Number.isFinite(value) ? [[key, Number((value / divisor).toFixed(2))]] : []; 11 }));

Source: speedtest-session.js

返回对象仅含有效且可更新的字段。界面用 Object.assign 合并该对象,因此没有正式样本的字段不会被 SDK 的空样本零值覆盖。下载和上传除以 1e6,采用十进制 Mb/s;各指标舍入到两位小数后再转换为数值,不保证始终显示两个小数位。

实时更新在 finished 或 error 状态下直接返回;读取与图表更新异常仅输出日志。图表调用同时获得四个显示值和 results.raw,但图表内部采样、绘制和销毁机制未在本页读取范围内。

依据:SpeedTest.vue。

进度是阶段状态,不是流量百分比

正式测试将延迟、下载和上传各分配 100 / 3 的权重:未开始为零、开始但未完成为半阶段、完成为整阶段。预览不参加该计算。界面收到运行通知时会先把进度提升到至少 5%,表示已经启动。

进度使用旧值与新值的最大值,因此不会回退,并将计算值限制在 100% 内。这是阶段式反馈,不是按已发送字节、耗时或样本数量计算的准确完成比例。

依据:SpeedTest.vue、SpeedTest.vue。

最终汇总与实时更新的语义不同

最终结果直接来自 results.getSummary():

字段最终转换缺失值处理
downloadSpeed、uploadSpeeddownload、upload 除以 1,000,000,保留至两位小数null / undefined 变为 0
latency、jitter使用同名摘要值,保留至两位小数null / undefined 变为 0
downLoadedLatency、upLoadedLatency保留至两位小数保留 '-'

因此最终基本指标的 0 可能表示没有样本,而不是确实测得零;负载延迟则保留明确的未测占位符。实时阶段使用 Number.isFinite 筛选,但最终格式化函数只专门处理空值,这两条路径的校验强度并不相同。

依据:SpeedTest.vue。

完成判定、评分与事件输出

完成回调不等于成功

组件明确考虑 SDK 在所有请求失败后仍触发 onFinish 的情况。以下原始代码先读取摘要,再检查下载、上传、延迟是否至少有一个有限正数:

javascript
1 const summary = results?.getSummary?.() ?? {}; 2 const measured = (v) => Number.isFinite(v) && v > 0; 3 const hasData = [summary.download, summary.upload, summary.latency].some(measured); 4 if (state.speedTest.status === 'error' || !hasData) { 5 state.speedTest.status = 'error'; 6 testEngine.onRunningChange = () => {}; 7 testEngine.onResultsChange = () => {}; 8 testEngine.onError = () => {}; 9 testEngine = null; 10 return; 11 }

Source: SpeedTest.vue

失败分支不发出成功事件,也不将进度强行设为完成。成功分支则设为 finished、进度设为 100、关闭部分迟到回调并更新最终结果。

这里是“至少一个有效测量”的成功标准,而不是“所有阶段都有有效测量”。例如只有延迟为有限正数、上传下载没有样本,仍可通过 hasData 检查;抖动不独立参与成功判定。

体验评分包含乐观丢包假设

最终评分来自 SDK 的 getScores(),包括 streaming、gaming 和 rtc 的分数与分类名称。调用前会改写结果对象的 getSummary:

javascript
const origGetSummary = results.getSummary.bind(results); results.getSummary = () => ({ ...origGetSummary(), packetLoss: 0 }); const scores = results.getScores();

Source: SpeedTest.vue

源码注释说明此流程不执行需要 TURN 配置的丢包测量,并希望避免 SDK 对未测丢包施加评分惩罚。因而这里注入的 packetLoss: 0 是评分假设,不是实测零丢包。解释游戏或实时通信质量时必须保留这一限制。

仅在 scores?.streaming 存在时,组件设置 hasScores 并读取三个体验类别;这一分支也依赖 SDK 同时提供 gaming、rtc 对象。

依据:SpeedTest.vue。

speedtest:finished 事件契约

成功处理在释放 testEngine 引用后设置 hasEverSettled,并发送事件。负载包含:

数据内容
基础测量downloadSpeed、uploadSpeed、latency、jitter
负载延迟downLoadedLatency、upLoadedLatency,可能为 '-'
scores{ streaming, gaming, rtc },无评分时为 null
qualities同三个键的分类名称,无评分时为 null
connectionstate.connection 的浅拷贝

源码注释将此事件与成就和报告消费者联系起来;本页只保证生产端的字段与触发条件。报告数据库、缓存、历史保存、导出格式和消费者过滤行为未从对应实现核验,不能据此宣称测速结果已经持久化。

依据:SpeedTest.vue。

配置选项

以下区分会话内部固定配置与调用方传入值,避免将预览参数误认为正式测试默认值。

配置项类型默认值或本实现赋值作用
packages.latency.count预期为 number调用方提供;默认值未核验正式测量 numPackets
packages.download.bytes预期为 number调用方提供;默认值未核验正式下载单次字节配置
packages.download.count预期为 number调用方提供;默认值未核验正式下载次数配置
packages.upload.bytes预期为 number调用方提供;默认值未核验正式上传单次字节配置
packages.upload.count预期为 number调用方提供;默认值未核验正式上传次数配置
autoStartboolean两阶段均显式为 false由会话显式控制启动
预览 latency.numPacketsnumber2固定的小规模延迟采样
预览 download.bytes / countnumber1e5 / 1固定预览下载
预览 bypassMinDurationbooleantrue跳过该测量的最小时长限制
预览 bandwidthMinRequestDurationnumber0允许快速预览产生带宽读数
预览 measureDownloadLoadedLatencybooleanfalse关闭预览下载负载延迟
预览 measureUploadLoadedLatencybooleanfalse关闭预览上传负载延迟
预览 logAimApiUrlnullnull禁用预览的该日志目标

配置证据:speedtest-session.js、speedtest-session.js。预览未配置日志目标不等于整个测速过程没有任何外部日志;正式阶段的 SDK 默认行为未读取。

内部 API 参考与用法

这些是 JavaScript 函数和回调,不是 HTTP API。源码没有静态类型声明,下述参数类型说明来自实际字段访问,不构造额外签名。

createSpeedTestSession(Engine, packages)

  • 参数:Engine 是可通过 new Engine(options) 实例化的构造器;packages 提供上一节列出的三个阶段配置。
  • 返回:具有 results、isProbe getter、三个控制方法及五个可覆盖回调的会话对象。
  • 控制方法:play()、pause()、destroy(),均不显式返回业务值。
  • 回调:onRunningChange(boolean)、onPhaseChange(phase)、onResultsChange()、onFinish(results)、onError(error)。
  • 阶段值:预览阶段统一为 'probe';正式阶段转发 SDK 的 measurement.type。
  • 异常:构造、配置访问和引擎方法调用没有在该封装内统一捕获;不存在可从源码保证的自定义异常类型。

组件中的真实构造用法如下:

javascript
reset() { return markRaw(createSpeedTestSession(SpeedTestEngine, state.config.package)); },

Source: SpeedTest.vue

getSpeedTestSampleCounts(raw)

返回包含 latency、download、upload 的样本计数对象。缺失顶层测量结果时按零处理;但 raw 本身必须存在,存在的下载、上传 bucket 也需要具有 timings 数组。它不是任意外部输入的通用验证器。

getSpeedTestLiveValues(results)

接受具有 raw 和对应 getter 的 SDK 结果对象,返回只包含可用指标的局部更新对象。无样本和非有限值被省略;基础延迟与抖动分别调用 getUnloadedLatency() 和 getUnloadedJitter()。

上述两个函数依据:speedtest-session.js。

失败模式、边界情况与并发

过期回调隔离

prepare() 为每一个实际引擎保存 current,每次转发前检查 !disposed && engine === current。因此已销毁会话的回调,或预览引擎在正式引擎接管后的迟到回调,不再转发。

detach() 把所有引擎回调替换为空函数而非 null;组件完成处理也采用类似方式。这样可以兼容 SDK 定时器无条件调用回调的行为。该机制是异步事件隔离,不是线程锁,也不能被解读为已证明 SDK 取消了每一个在途网络请求。

依据:speedtest-session.js、speedtest-session.js、SpeedTest.vue。

两层错误处理的差异

情形会话层界面层
任意当前引擎错误将 failed 置为 true 并转发所有错误写入控制台
不含 ICE 的字符串错误同上立即设为 error
含 ICE 的字符串、非字符串错误同上不在 onError 中立即修改状态
预览失败后触发结束不再启动正式引擎,转发完成结果由已有错误状态与 hasData 决定成功或失败
没有任何有限正数的下载、上传、延迟转发完成判为失败,不发送成功事件
trace 请求或 IP 校验失败与测速会话无直接耦合连接查询记录日志并返回 null

一个值得注意的边界是:会话层对任何错误都设置 failed,但界面只对一部分错误立即进入错误状态。因此,被界面忽略的预览错误仍会阻止正式阶段启动;若预览结果含有效数据且界面未处于 error,完成判定可能仍通过。不能把 ICE 过滤描述为“错误对整个流程完全无影响”。

依据:speedtest-session.js、SpeedTest.vue、SpeedTest.vue。

轮次与连接信息

控制器开始新轮次时增加 runId;连接查询只在返回时仍匹配该轮次才落入状态,从而避免旧异步查询覆盖新一轮信息。但 loadConnection 在已有 state.connection.ip 时跳过查询,因此仅依据该方法不能保证每次重新测速都会刷新出口 IP。网络、代理或出口切换后,应核查调用方是否重置了连接状态。

依据:SpeedTest.vue、SpeedTest.vue。

性能、运维与扩展建议

  • 流量成本:预览至少配置一次 100,000 字节下载,并额外执行延迟请求;正式流量取决于调用方的 bytes 与 count。源码未展开 SDK 的额外采样和请求规则,不能仅凭配置推导精确总流量。
  • 实时计算:每次结果变更会重新遍历下载、上传的结果 bucket 统计样本,再更新指标和图表。已读的更新路径没有额外节流;实际回调频率由 SDK 决定。
  • 排障入口:区分连接查询日志、updateSpeedInRealTime 日志和测速引擎错误日志。连接信息失败与无测速样本是不同故障,不应混为一谈。
  • 超时与重试:trace 请求使用 fetchWithTimeout,但默认超时值及内部重试未读取;会话封装没有显式重试循环。不能据此断言 SDK 没有自己的重试。
  • 扩展引擎:Engine 作为参数传入,便于替换实现或提供测试替身。替代实现必须匹配构造选项、回调、控制方法和 results 数据形状;不能只实现 play() 和 pause()。
  • 扩展指标:新增测量需要同步修改正式测量列表、样本门控、最终摘要转换、进度权重和事件负载。当前进度硬编码为三个阶段,添加第四阶段而不调整权重会使进度失真。
  • 评分说明:若未来增加真实丢包测量,应重新审视 packetLoss: 0 的覆盖逻辑,避免实际结果仍被乐观假设替换。

这些建议分别基于已述会话配置、实时更新、三阶段进度和评分路径,不表示仓库已经实现相应扩展。

测试证据边界

本次限额内未读取测速会话专用测试,不能宣称暂停、预览失败、重复启动或过期回调已有自动化覆盖。适合后续核验的场景包括:预览不污染正式原始样本、正式空样本不覆盖预览值、抖动至少两个样本、销毁后的迟到回调被忽略,以及“存在少量有效数据但某阶段失败”的最终判定。这些是由控制流导出的测试建议,而非已确认测试清单。

相关链接

Sources

(2 files)
frontend/components
frontend/utils