Report: crossing readings from traceroutes and cable owners; flat-fit section removed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Toby
2026-10-27 20:19:08 -04:00
parent c094eb88fa
commit df29028ae8
9 changed files with 178 additions and 181 deletions
File diff suppressed because one or more lines are too long
+43 -1
View File
@@ -157,6 +157,19 @@ tg = {}
for f in glob.glob(P("raw/tg/cables/*.json")): for f in glob.glob(P("raw/tg/cables/*.json")):
c = json.load(open(f)) c = json.load(open(f))
tg[c["id"]] = c tg[c["id"]] = c
CCN = {"NZ": "New Zealand", "AU": "Australia", "AO": "Angola", "BR": "Brazil", "NC": "New Caledonia", "SG": "Singapore", "US": "United States"}
anc_cc = {x["fqdn"].split(".")[0]: x["country"] for x in json.load(open(P("raw/ripe_anchors_all.json")))}
toks_of = lambda cid: {w for own in ((tg.get(cid) or {}).get("owners") or "").split(",") for w in re.findall(r"[a-z0-9]+", own.lower()) if w not in STOP and len(w) > 1}
owns = lambda cid, o: o is not None and any(re.search(rf"(^|[^a-z0-9]){t}([^a-z0-9]|$)", (asname(o) or "").lower()) for t in toks_of(cid))
def short(o):
"""'VOCUS-BACKBONE-AS Vocus Connect International Backbone, AU' -> 'Vocus Connect International Backbone'."""
n = re.sub(r",\s*[A-Z]{2}$", "", asname(o) or "")
m = re.match(r"^(\S+)\s+(?:-\s+)?(.+)$", n)
return (m.group(2) if m else n).strip()
xing = [] xing = []
for c in crossings: for c in crossings:
hops = c["hops"] hops = c["hops"]
@@ -172,7 +185,36 @@ for c in crossings:
owners = (tg.get(c["cable"]) or {}).get("owners") or "" owners = (tg.get(c["cable"]) or {}).get("owners") or ""
toks = {w for own in owners.split(",") for w in re.findall(r"[a-z0-9]+", own.lower()) if w not in STOP and len(w) > 1} toks = {w for own in owners.split(",") for w in re.findall(r"[a-z0-9]+", own.lower()) if w not in STOP and len(w) > 1}
is_owner = lambda o: o is not None and any(re.search(rf"(^|[^a-z0-9]){t}([^a-z0-9]|$)", (asname(o) or "").lower()) for t in toks) is_owner = lambda o: o is not None and any(re.search(rf"(^|[^a-z0-9]){t}([^a-z0-9]|$)", (asname(o) or "").lower()) for t in toks)
xing.append(dict(src=c["src"], dst=c["dst"], cable=c["cable"], owners=owners or None, jump_ms=c["jump"], # hops around the jump: last announced hop before it, exchange points, first announced hop after it
ib = next(i for i in range(c["before"], -1, -1) if hops[i]["ip"] and look.get(hops[i]["ip"], (None,))[0])
ia = next((i for i in range(c["after"], len(hops)) if hops[i]["ip"] and look.get(hops[i]["ip"], (None,))[0]), len(hops) - 1)
path, seen = [], set()
for i in range(ib, ia + 1):
ip = hops[i]["ip"]
o = look.get(ip, (None,))[0] if ip else None
lab = f"AS{o} {short(o)}" if o else (ixp_of[ip] + " exchange" if ip in ixp_of else None)
if lab and (not path or path[-1] != lab):
path.append(lab)
if i == c["before"]:
path.append(f"{c['jump']} ms")
ca, cb = anc_cc.get(c["src"]), anc_cc.get(c["dst"])
ixp_after = next((ixp_of[hops[i]["ip"]] for i in range(c["after"], ia + 1) if hops[i]["ip"] in ixp_of), None)
if ca == cb:
reading = f"Overland within {CCN.get(ca, ca)}; {short(ob)} at both ends."
elif owners and is_owner(ob) and is_owner(oa):
reading = f"Carried end to end by {short(ob)}, the cable's owner."
elif owners and is_owner(ob):
reading = f"Carried across by {short(ob)}, the cable's owner, and handed over to {short(oa)}" + (f" at the {ixp_after.split(' (')[0]} exchange." if ixp_after else ".")
else:
cands = sorted(cid for cid, t in tg.items() if not t.get("is_planned") and (t.get("rfs_year") or 0) <= 2026
and {CCN.get(ca), CCN.get(cb)} <= {lp.get("country") for lp in t.get("landing_points", [])})
owned = [cid for cid in cands if owns(cid, ob) or owns(cid, oa)]
cnames = ", ".join(tg[cid]["name"].replace(" Cable", "") for cid in cands)
carrier = short(ob) if ob == oa else f"{short(ob)} and {short(oa)}"
reading = (f"{carrier} operates both ends and owns none of the {len(cands)} cables between {CCN.get(ca)} and {CCN.get(cb)}"
+ (f" ({cnames})" if len(cands) <= 5 else "") + "; the crossing runs on capacity held on one of them, the usual arrangement for carriers.") if not owned else \
f"{carrier} operates both ends and is an owner of {', '.join(tg[cid]['name'] for cid in owned)}."
xing.append(dict(src=c["src"], dst=c["dst"], cable=c["cable"], owners=owners or None, jump_ms=c["jump"], path=path, reading=reading,
before=dict(hop=hb, asn=ob, name=asname(ob), owner=is_owner(ob) if owners else None), before=dict(hop=hb, asn=ob, name=asname(ob), owner=is_owner(ob) if owners else None),
after=dict(hop=ha, asn=oa, name=asname(oa), owner=is_owner(oa) if owners else None))) after=dict(hop=ha, asn=oa, name=asname(oa), owner=is_owner(oa) if owners else None)))
+1 -1
View File
@@ -204,7 +204,7 @@ def nets():
ends.sort(key=lambda e: (e["kind"], e["city"])) ends.sort(key=lambda e: (e["kind"], e["city"]))
cname = {c["id"]: c["name"].replace(" Cable", "") for c in json.load(open(P("data/cables.json")))["cables"]} cname = {c["id"]: c["name"].replace(" Cable", "") for c in json.load(open(P("data/cables.json")))["cables"]}
xs = [dict(src_city=city(x["src"].split(":")[-1]), dst_city=city(x["dst"].split(":")[-1]), cable_name=cname.get(x["cable"]) if x["cable"] else None, xs = [dict(src_city=city(x["src"].split(":")[-1]), dst_city=city(x["dst"].split(":")[-1]), cable_name=cname.get(x["cable"]) if x["cable"] else None,
owners=x["owners"], before=x["before"], after=x["after"]) for x in a["crossings"]] owners=x["owners"], path=x["path"], reading=x["reading"], assumed=x["cable"] == "tasman-global-access-tga-cable") for x in a["crossings"]]
return dict(summary=a["summary"], traces=a["traces"], endpoints=ends, gp=sorted(gp.values(), key=lambda x: -x["probes"]), return dict(summary=a["summary"], traces=a["traces"], endpoints=ends, gp=sorted(gp.values(), key=lambda x: -x["probes"]),
transit=a["transit"], ixps=a["ixps"], crossings=xs) transit=a["transit"], ixps=a["ixps"], crossings=xs)
+64 -15
View File
@@ -74,6 +74,12 @@
"cable": "tasman-global-access-tga-cable", "cable": "tasman-global-access-tga-cable",
"owners": "One NZ, Spark New Zealand, Telstra", "owners": "One NZ, Spark New Zealand, Telstra",
"jump_ms": 23.35, "jump_ms": 23.35,
"path": [
"AS4826 Vocus Connect International Backbone",
"23.35 ms",
"AS4826 Vocus Connect International Backbone"
],
"reading": "Vocus Connect International Backbone operates both ends and owns none of the 4 cables between New Zealand and Australia (Hawaiki, Southern Cross Network (SCCN), Southern Cross NEXT, Tasman Global Access (TGA)); the crossing runs on capacity held on one of them, the usual arrangement for carriers.",
"before": { "before": {
"hop": 2, "hop": 2,
"asn": "4826", "asn": "4826",
@@ -93,6 +99,12 @@
"cable": "tasman-global-access-tga-cable", "cable": "tasman-global-access-tga-cable",
"owners": "One NZ, Spark New Zealand, Telstra", "owners": "One NZ, Spark New Zealand, Telstra",
"jump_ms": 23.2, "jump_ms": 23.2,
"path": [
"AS4826 Vocus Connect International Backbone",
"23.2 ms",
"AS4826 Vocus Connect International Backbone"
],
"reading": "Vocus Connect International Backbone operates both ends and owns none of the 4 cables between Australia and New Zealand (Hawaiki, Southern Cross Network (SCCN), Southern Cross NEXT, Tasman Global Access (TGA)); the crossing runs on capacity held on one of them, the usual arrangement for carriers.",
"before": { "before": {
"hop": 1, "hop": 1,
"asn": "4826", "asn": "4826",
@@ -112,6 +124,12 @@
"cable": "south-atlantic-cable-system-sacs", "cable": "south-atlantic-cable-system-sacs",
"owners": "Angola Cables", "owners": "Angola Cables",
"jump_ms": 60.59, "jump_ms": 60.59,
"path": [
"AS37468 ANGOLA-CABLES",
"60.59 ms",
"AS37468 ANGOLA-CABLES"
],
"reading": "Carried end to end by ANGOLA-CABLES, the cable's owner.",
"before": { "before": {
"hop": 3, "hop": 3,
"asn": "37468", "asn": "37468",
@@ -131,6 +149,12 @@
"cable": "south-atlantic-cable-system-sacs", "cable": "south-atlantic-cable-system-sacs",
"owners": "Angola Cables", "owners": "Angola Cables",
"jump_ms": 60.79, "jump_ms": 60.79,
"path": [
"AS37468 ANGOLA-CABLES",
"60.79 ms",
"AS37468 ANGOLA-CABLES"
],
"reading": "Carried end to end by ANGOLA-CABLES, the cable's owner.",
"before": { "before": {
"hop": 1, "hop": 1,
"asn": "37468", "asn": "37468",
@@ -150,6 +174,13 @@
"cable": "gondwana-1", "cable": "gondwana-1",
"owners": "OPT", "owners": "OPT",
"jump_ms": 21.39, "jump_ms": 21.39,
"path": [
"AS18200 Office des Postes et Telecommunications New-Caledonia",
"21.39 ms",
"Equinix Sydney (Sydney, AU) exchange",
"AS4826 Vocus Connect International Backbone"
],
"reading": "Carried across by Office des Postes et Telecommunications New-Caledonia, the cable's owner, and handed over to Vocus Connect International Backbone at the Equinix Sydney exchange.",
"before": { "before": {
"hop": 3, "hop": 3,
"asn": "18200", "asn": "18200",
@@ -169,6 +200,12 @@
"cable": "australia-singapore-cable-asc", "cable": "australia-singapore-cable-asc",
"owners": "Vocus Communications", "owners": "Vocus Communications",
"jump_ms": 50.57, "jump_ms": 50.57,
"path": [
"AS4826 Vocus Connect International Backbone",
"50.57 ms",
"AS4826 Vocus Connect International Backbone"
],
"reading": "Carried end to end by Vocus Connect International Backbone, the cable's owner.",
"before": { "before": {
"hop": 1, "hop": 1,
"asn": "4826", "asn": "4826",
@@ -188,6 +225,12 @@
"cable": null, "cable": null,
"owners": null, "owners": null,
"jump_ms": 45.28, "jump_ms": 45.28,
"path": [
"AS4826 Vocus Connect International Backbone",
"45.28 ms",
"AS4826 Vocus Connect International Backbone"
],
"reading": "Overland within Australia; Vocus Connect International Backbone at both ends.",
"before": { "before": {
"hop": 1, "hop": 1,
"asn": "4826", "asn": "4826",
@@ -207,6 +250,12 @@
"cable": null, "cable": null,
"owners": null, "owners": null,
"jump_ms": 148.88, "jump_ms": 148.88,
"path": [
"AS6939 Hurricane Electric LLC",
"148.88 ms",
"AS6939 Hurricane Electric LLC"
],
"reading": "Hurricane Electric LLC operates both ends and owns none of the 4 cables between Australia and United States (Hawaiki, Southern Cross Network (SCCN), Southern Cross NEXT, Telstra Endeavour); the crossing runs on capacity held on one of them, the usual arrangement for carriers.",
"before": { "before": {
"hop": 8, "hop": 8,
"asn": "6939", "asn": "6939",
@@ -4025,19 +4074,14 @@
"name": "LEVEL3 - Level 3 Parent, LLC, US", "name": "LEVEL3 - Level 3 Parent, LLC, US",
"traces": 33 "traces": 33
}, },
{
"asn": "45177",
"name": "DEVOLI-AS-AP Devoli, NZ",
"traces": 24
},
{ {
"asn": "38064", "asn": "38064",
"name": "NZ-AS-NS1-AP ASN for anycast peering of, NZ", "name": "NZ-AS-NS1-AP ASN for anycast peering of, NZ",
"traces": 24 "traces": 24
}, },
{ {
"asn": "18200", "asn": "45177",
"name": "OPT-NC-AS-AP Office des Postes et Telecommunications New-Caledonia, NC", "name": "DEVOLI-AS-AP Devoli, NZ",
"traces": 24 "traces": 24
}, },
{ {
@@ -4045,19 +4089,24 @@
"name": "NAUTILE-NC-AS-AP Nautile, NC", "name": "NAUTILE-NC-AS-AP Nautile, NC",
"traces": 24 "traces": 24
}, },
{
"asn": "18200",
"name": "OPT-NC-AS-AP Office des Postes et Telecommunications New-Caledonia, NC",
"traces": 24
},
{ {
"asn": "64112", "asn": "64112",
"name": "PIT CHILE SP, CL", "name": "PIT CHILE SP, CL",
"traces": 24 "traces": 24
}, },
{ {
"asn": "12956", "asn": "4270",
"name": "TELXIUS TELEFONICA GLOBAL SOLUTIONS SL, ES", "name": "Red de Interconexion Universitaria, AR",
"traces": 24 "traces": 24
}, },
{ {
"asn": "4270", "asn": "12956",
"name": "Red de Interconexion Universitaria, AR", "name": "TELXIUS TELEFONICA GLOBAL SOLUTIONS SL, ES",
"traces": 24 "traces": 24
}, },
{ {
@@ -4071,13 +4120,13 @@
"traces": 24 "traces": 24
}, },
{ {
"asn": "55685", "asn": "4622",
"name": "JLM-AS-ID PT Jala Lintas Media, ID", "name": "ID-NIC Indonesia Network Information Center, ID",
"traces": 24 "traces": 24
}, },
{ {
"asn": "4622", "asn": "55685",
"name": "ID-NIC Indonesia Network Information Center, ID", "name": "JLM-AS-ID PT Jala Lintas Media, ID",
"traces": 24 "traces": 24
}, },
{ {
+13 -55
View File
File diff suppressed because one or more lines are too long
+12 -54
View File
@@ -304,21 +304,11 @@ summary{cursor:pointer;font:600 14px var(--display);text-transform:uppercase;let
<p class="note" id="netnote"></p> <p class="note" id="netnote"></p>
</section> </section>
<section>
<h2 class="sec">Best attainable flat arrangements</h2>
<p class="lede">Latency bounds distances only from above, so a free flat arrangement can satisfy any set of measurements by shrinking. The informative question is how far a given flat map would have to be corrected. Each city may move at most the stated distance from where the map places it; the arrangement shown minimises the total excess over all measured pairs at the speed of light in fibre, using straight lines (the shortest possible cable). The final setting is the smallest correction at which no route remains inconsistent. The problem is convex, so this minimum is global.</p>
<div class="ffctl"><label for="ffb">Maximum correction per city</label><input id="ffb" type="range" min="0" max="6" step="1" value="0"><output id="ffbv" for="ffb"></output></div>
<div class="ff" id="ff"></div>
<div class="ffkey"><span><i class="o"></i>position on the map</span><span><i class="f"></i>corrected position</span><span><i style="background:var(--dim)"></i>correction</span><span><i style="background:var(--fib)"></i>faster than light in fibre</span><span><i style="background:var(--bad)"></i>faster than light in vacuum</span></div>
<div class="tw" style="margin-top:16px"><table class="mini" id="fftab"></table></div>
<p class="note" id="ffnote"></p>
</section>
<section> <section>
<h2 class="sec">Independent verification</h2> <h2 class="sec">Independent verification</h2>
<p class="lede">To reproduce a measurement, ping one of the anchors from any computer and enter the minimum round-trip time. Local network and access delays can only increase the measured value.</p> <p class="lede">To reproduce a measurement, ping one of the anchors from any computer and enter the minimum round-trip time. Local network and access delays can only increase the measured value.</p>
<form id="byo" class="byo" autocomplete="off"> <form id="byo" class="byo" autocomplete="off">
<label for="byoA">Location<input id="byoA" list="byocities" placeholder="City, or lat, lon"></label> <label for="byoA">Location<input id="byoA" list="byocities" placeholder="Type a city, or lat, lon"></label>
<label for="byoB">Anchor<select id="byoB"></select></label> <label for="byoB">Anchor<select id="byoB"></select></label>
<label for="byoT">Minimum RTT, ms<input id="byoT" type="number" min="0" step="0.1"></label> <label for="byoT">Minimum RTT, ms<input id="byoT" type="number" min="0" step="0.1"></label>
<button class="go" type="submit">Evaluate</button> <button class="go" type="submit">Evaluate</button>
@@ -704,7 +694,7 @@ function setSource(src){
const worst=LP.slice().sort((x,y)=>(rt(y).fl-y.T)-(rt(x).fl-x.T))[0]; const worst=LP.slice().sort((x,y)=>(rt(y).fl-y.T)-(rt(x).fl-x.T))[0];
S.sel=(LP.find(p=>p.a+"-"+p.b===key)||LP.find(p=>(p.a===ia&&p.b===ib)||(p.a===ib&&p.b===ia))||worst).idx; S.sel=(LP.find(p=>p.a+"-"+p.b===key)||LP.find(p=>(p.a===ia&&p.b===ib)||(p.a===ib&&p.b===ia))||worst).idx;
document.getElementById("srcsel").value=src; document.getElementById("srcsel").value=src;
bgKey="";readout();renderCard();renderScore();drawSM();drawSpeed();renderLadder();renderEx();renderMethod();renderFlat();frame(); bgKey="";readout();renderCard();renderScore();drawSM();drawSpeed();renderLadder();renderEx();renderMethod();frame();
} }
document.getElementById("srcsel").onchange=e=>setSource(e.target.value); document.getElementById("srcsel").onchange=e=>setSource(e.target.value);
function setMode(md){S.mode=md;document.querySelectorAll("#mode button").forEach(b=>b.classList.toggle("on",b.dataset.mode===md));legend();readout();renderCard();renderScore();drawSM();drawSpeed();frame()} function setMode(md){S.mode=md;document.querySelectorAll("#mode button").forEach(b=>b.classList.toggle("on",b.dataset.mode===md));legend();readout();renderCard();renderScore();drawSM();drawSpeed();frame()}
@@ -895,6 +885,8 @@ function byoInit(){
const sel=document.getElementById("byoB");sel.innerHTML=BA.map((a,i)=>`<option value="${i}">${esc(a[0])}, ${a[1]} · ${a[4]}</option>`).join(""); const sel=document.getElementById("byoB");sel.innerHTML=BA.map((a,i)=>`<option value="${i}">${esc(a[0])}, ${a[1]} · ${a[4]}</option>`).join("");
sel.value=String(BA.findIndex(a=>a[0]==="Sydney"));document.getElementById("byoA").value="Auckland, NZ";document.getElementById("byoT").value="25.2"; sel.value=String(BA.findIndex(a=>a[0]==="Sydney"));document.getElementById("byoA").value="Auckland, NZ";document.getElementById("byoT").value="25.2";
sel.onchange=byoCmd;document.getElementById("byo").addEventListener("submit",byoRun);byoCmd();byoRun(); sel.onchange=byoCmd;document.getElementById("byo").addEventListener("submit",byoRun);byoCmd();byoRun();
/* the example location clears on first focus, so the full city list appears while typing */
document.getElementById("byoA").addEventListener("focus",e=>{if(byoExample&&e.target.value==="Auckland, NZ")e.target.value=""},{once:true});
} }
/* ---------- raw records, code and data ---------- */ /* ---------- raw records, code and data ---------- */
@@ -907,7 +899,7 @@ function renderCode(){
const gp=D.live.runs.filter(r=>!r.source),vps=[...new Set(D.live.cells.filter(c=>/Vultr/.test(c[5])).map(c=>c[6]))].sort(); const gp=D.live.runs.filter(r=>!r.source),vps=[...new Set(D.live.cells.filter(c=>/Vultr/.test(c[5])).map(c=>c[6]))].sort();
const f=(p,t)=>lnk(SRCV+"src/"+p,t||p),py=[["models.py","distance on each model, constants"],["measure.py","Globalping measurements"],["vps.py","Vultr servers: create, measure, collect, delete"],["vps_analyze.py","Vultr logs to measurements"], const f=(p,t)=>lnk(SRCV+"src/"+p,t||p),py=[["models.py","distance on each model, constants"],["measure.py","Globalping measurements"],["vps.py","Vultr servers: create, measure, collect, delete"],["vps_analyze.py","Vultr logs to measurements"],
["analyze_wn.py","WonderNetwork archive: statistics, mislocated servers"],["cables.py","published cable lengths on each model"],["network.py","cable-and-land routing on each model"],["trace.py","RIPE Atlas sea crossings"], ["analyze_wn.py","WonderNetwork archive: statistics, mislocated servers"],["cables.py","published cable lengths on each model"],["network.py","cable-and-land routing on each model"],["trace.py","RIPE Atlas sea crossings"],
["flatfit.py","best attainable flat arrangements"],["asmap.py","network attribution from public BGP data"],["pingmap.py","shape fit without positions"],["build.py","this page"],["export.py","JSON data hand-off"]]; ["asmap.py","network attribution from public BGP data"],["pingmap.py","shape fit without positions"],["build.py","this page"],["export.py","JSON data hand-off"]];
document.getElementById("code").innerHTML=` document.getElementById("code").innerHTML=`
<div>Repository</div><div>${lnk(GIT,GIT.replace("https://",""))}</div> <div>Repository</div><div>${lnk(GIT,GIT.replace("https://",""))}</div>
<div>Data hand-off</div><div>${lnk(RAWV+"cable_latency_survey_data_v2.json","cable_latency_survey_data_v2.json")} (every measurement, site, route and cable path) · ${lnk(RAWV+"cable_latency_survey_data.json","version 1")}</div> <div>Data hand-off</div><div>${lnk(RAWV+"cable_latency_survey_data_v2.json","cable_latency_survey_data_v2.json")} (every measurement, site, route and cable path) · ${lnk(RAWV+"cable_latency_survey_data.json","version 1")}</div>
@@ -916,14 +908,13 @@ function renderCode(){
<div>Residential connection</div><div>${f("data/home/ping.log","ping log")}</div> <div>Residential connection</div><div>${f("data/home/ping.log","ping log")}</div>
<div>RIPE Atlas</div><div>${f("data/trace.json","sea-crossing traceroutes")} (hops, timings, measurement and probe IDs)</div> <div>RIPE Atlas</div><div>${f("data/trace.json","sea-crossing traceroutes")} (hops, timings, measurement and probe IDs)</div>
<div>WonderNetwork</div><div>${lnk("https://wp-public-data.s3.amazonaws.com/pings/pings-2020-07-19-2020-07-20.csv.gz","original archive")} · ${f("data/wn_clean.json","per-pair statistics")}</div> <div>WonderNetwork</div><div>${lnk("https://wp-public-data.s3.amazonaws.com/pings/pings-2020-07-19-2020-07-20.csv.gz","original archive")} · ${f("data/wn_clean.json","per-pair statistics")}</div>
<div>Derived</div><div>${f("data/network.json","routes")} · ${f("data/cables.json","cables")} · ${f("data/flatfit.json","flat arrangements")} · ${f("data/asmap.json","networks")} · ${f("data/pingmap.json","shape fit")}</div> <div>Derived</div><div>${f("data/network.json","routes")} · ${f("data/cables.json","cables")} · ${f("data/asmap.json","networks")} · ${f("data/pingmap.json","shape fit")}</div>
<div>Pipeline</div><div>${py.map(([p,t])=>`${f(p)} ${t}`).join("<br>")}</div> <div>Pipeline</div><div>${py.map(([p,t])=>`${f(p)} ${t}`).join("<br>")}</div>
<div>Licences</div><div>${f("../LICENSE","MIT")} (code) · ${f("../LICENSE-DATA.md","CC BY 4.0 and third-party terms")} (data)</div>`; <div>Licences</div><div>${f("../LICENSE","MIT")} (code) · ${f("../LICENSE-DATA.md","CC BY 4.0 and third-party terms")} (data)</div>`;
document.getElementById("repro").textContent=`git clone ${GIT}.git document.getElementById("repro").textContent=`git clone ${GIT}.git
cd cable-latency-survey/src cd cable-latency-survey/src
./fetch_raw.sh # third-party inputs that are not redistributed ./fetch_raw.sh # third-party inputs that are not redistributed
python3 build.py # this page, from the files in data/ python3 build.py # this page, from the files in data/
python3 flatfit.py # flat arrangements (requires cvxpy)
python3 export.py # the JSON data hand-off`; python3 export.py # the JSON data hand-off`;
} }
@@ -940,44 +931,11 @@ function renderNet(){
document.getElementById("netanc").innerHTML=`<thead><tr><th>Anchor</th><th>Host, per RIPE Atlas</th><th>BGP origin</th><th>RPKI</th></tr></thead><tbody>`+anc.map(e=>`<tr><td>${esc(e.city)}<br><span style="color:var(--dim)">${esc(e.id)}</span></td><td>${/^\d*$/.test(e.company)?"not stated":esc(e.company)} (${asn(e.claimed_asn)})</td><td class="${e.match?"":"fib"}">${asn(e.bgp_origin)} ${esc(e.name)}<br><span style="color:var(--dim)">${esc(e.prefix)}</span></td><td>${e.rpki.join(", ")}</td></tr>`).join("")+"</tbody>"; document.getElementById("netanc").innerHTML=`<thead><tr><th>Anchor</th><th>Host, per RIPE Atlas</th><th>BGP origin</th><th>RPKI</th></tr></thead><tbody>`+anc.map(e=>`<tr><td>${esc(e.city)}<br><span style="color:var(--dim)">${esc(e.id)}</span></td><td>${/^\d*$/.test(e.company)?"not stated":esc(e.company)} (${asn(e.claimed_asn)})</td><td class="${e.match?"":"fib"}">${asn(e.bgp_origin)} ${esc(e.name)}<br><span style="color:var(--dim)">${esc(e.prefix)}</span></td><td>${e.rpki.join(", ")}</td></tr>`).join("")+"</tbody>";
document.getElementById("nettr").innerHTML=`<thead><tr><th>Network</th><th>Traceroutes</th></tr></thead><tbody>`+N.transit.map(x=>`<tr><td>${asn(x.asn)} ${esc(x.name)}</td><td class="num">${x.traces}</td></tr>`).join("")+"</tbody>"; document.getElementById("nettr").innerHTML=`<thead><tr><th>Network</th><th>Traceroutes</th></tr></thead><tbody>`+N.transit.map(x=>`<tr><td>${asn(x.asn)} ${esc(x.name)}</td><td class="num">${x.traces}</td></tr>`).join("")+"</tbody>";
document.getElementById("netix").innerHTML=`<thead><tr><th>Exchange point (PeeringDB)</th><th>Hop addresses</th></tr></thead><tbody>`+N.ixps.map(x=>`<tr><td>${esc(x.name)}</td><td class="num">${x.addresses}</td></tr>`).join("")+"</tbody>"; document.getElementById("netix").innerHTML=`<thead><tr><th>Exchange point (PeeringDB)</th><th>Hop addresses</th></tr></thead><tbody>`+N.ixps.map(x=>`<tr><td>${esc(x.name)}</td><td class="num">${x.addresses}</td></tr>`).join("")+"</tbody>";
const side=s=>s.asn?`${asn(s.asn)} ${esc(s.name)}${s.owner===true?' · <span class="ok">cable owner</span>':s.owner===false?' · <span class="fib">not a listed owner</span>':""}`:"—"; document.getElementById("netx").innerHTML=`<thead><tr><th>Crossing</th><th>Cable and owners (TeleGeography)</th><th>Networks across the jump</th><th>Reading</th></tr></thead><tbody>`+
document.getElementById("netx").innerHTML=`<thead><tr><th>Crossing</th><th>Cable and owners (TeleGeography)</th><th>Last announced hop before the jump</th><th>First announced hop after it</th></tr></thead><tbody>`+ N.crossings.map(x=>`<tr><td>${esc(x.src_city)} → ${esc(x.dst_city)}</td><td>${x.cable_name?(x.assumed?`not confirmed; ${esc(x.cable_name)} assumed for its length`:`${esc(x.cable_name)}: ${esc(x.owners||"—")}`):"not identified"}</td><td>${x.path.map(esc).join(" → ")}</td><td>${esc(x.reading)}</td></tr>`).join("")+"</tbody>";
N.crossings.map(x=>`<tr><td>${esc(x.src_city)} → ${esc(x.dst_city)}</td><td>${x.cable_name?`${esc(x.cable_name)}: ${esc(x.owners||"—")}`:"not identified"}</td><td>${side(x.before)}</td><td>${side(x.after)}</td></tr>`).join("")+"</tbody>"; document.getElementById("netnote").innerHTML=`AS names are the registrants' designations. The two anchors announced by a different AS than their name states are both hosted by DigiCert, which operates both numbers. RPKI "unknown" means the address holder has published no route origin authorisation; no announcement was invalid. Cable owners are from TeleGeography; a network that operates both ends of a crossing without owning the cable holds capacity on it, which is how most carriers use submarine cables. Code: ${lnk(SRCV+"src/asmap.py","asmap.py")}; results: ${lnk(SRCV+"src/data/asmap.json","asmap.json")}, ${lnk(SRCV+"src/data/rpki.json","rpki.json")}.`;
document.getElementById("netnote").innerHTML=`AS names are the registrants' designations. The two anchors announced by a different AS than their name states are both hosted by DigiCert, which operates both numbers. RPKI "unknown" means the address holder has published no route origin authorisation; no announcement was invalid. Ownership tokens are matched against AS names, so a network that leases capacity on a cable appears as "not a listed owner". Code: ${lnk(SRCV+"src/asmap.py","asmap.py")}; results: ${lnk(SRCV+"src/data/asmap.json","asmap.json")}, ${lnk(SRCV+"src/data/rpki.json","rpki.json")}.`;
} }
/* ---------- best attainable flat arrangements ---------- */
let FFB=0;
const FFM=[["ae","North-pole AE","ae"],["dp","Dual-pole AE","dualpole"],["merc","Mercator","mercator"]];
function renderFlat(){
if(!D.flat)return;
const box=document.getElementById("ff"),F=D.flat.fits[S.src],bud=D.flat.budgets_km;
if(!box.children.length)box.innerHTML=FFM.map(([m,n])=>`<div><h3>${n}</h3><canvas id="ff-${m}" aria-label="Corrected ${n} arrangement"></canvas><p id="ffp-${m}"></p></div>`).join("");
document.getElementById("ffbv").textContent=FFB===0?"0 km (as mapped)":FFB<bud.length?`${fint(bud[FFB])} km`:"minimum for full consistency";
for(const [m,n,k] of FFM){
const f=F[k],row=f.budgets[Math.min(FFB,f.budgets.length-1)],c=document.getElementById("ff-"+m),r=c.getBoundingClientRect(),w=r.width,h=r.height;if(!w)continue;
c.width=Math.round(w*DPR);c.height=Math.round(h*DPR);const g=c.getContext("2d");g.setTransform(DPR,0,0,DPR,0,0);
const rad=w/2-8,proj=baseProj(m).scale(rad/Math.PI).translate([w/2,h/2]),path=d3.geoPath(proj,g);
g.fillStyle="#050a12";g.fillRect(0,0,w,h);
g.beginPath();if(m==="merc")g.rect(w/2-rad,h/2-rad,2*rad,2*rad);else g.arc(w/2,h/2,rad,0,2*Math.PI);g.fillStyle=C.deep;g.fill();g.strokeStyle="#1a2a40";g.lineWidth=1;g.stroke();
g.beginPath();path(grat);g.strokeStyle=C.grat;g.stroke();g.beginPath();path(landGeo);g.fillStyle=C.land;g.fill();
const at=f.sites.map(s=>proj([LS[s].lon,LS[s].lat])),to=row.pos.map(q=>proj(q)),ix=Object.fromEntries(f.sites.map((s,i)=>[s,i]));
g.lineWidth=1.5;for(const [a,b,t] of row.inconsistent){const p1=to[ix[a]],p2=to[ix[b]];g.strokeStyle=t===2?C.bad:C.fib;g.beginPath();g.moveTo(p1[0],p1[1]);g.lineTo(p2[0],p2[1]);g.stroke()}
g.strokeStyle=C.dim;g.lineWidth=1;for(let i=0;i<at.length;i++){if(Math.hypot(at[i][0]-to[i][0],at[i][1]-to[i][1])<1)continue;g.beginPath();g.moveTo(at[i][0],at[i][1]);g.lineTo(to[i][0],to[i][1]);g.stroke()}
for(let i=0;i<at.length;i++){g.beginPath();g.arc(at[i][0],at[i][1],3.5,0,2*Math.PI);g.strokeStyle=C.dim;g.lineWidth=1.2;g.stroke();g.beginPath();g.arc(to[i][0],to[i][1],2.6,0,2*Math.PI);g.fillStyle=C.sel;g.fill()}
const mv=f.sites.map((s,i)=>[Math.hypot(at[i][0]-to[i][0],at[i][1]-to[i][1]),i]).filter(x=>x[0]>6).sort((x,y)=>y[0]-x[0]).slice(0,4);
g.font=`11px ${FONT()}`;g.textAlign="left";const boxes=[];for(const [,i] of mv){const t=LS[f.sites[i]].name.replace(/ \(.*\)$/,""),tw=g.measureText(t).width,x=Math.max(4,Math.min(to[i][0]+6,w-6-tw)),y=to[i][1]-6,bx=[x-2,y-11,x+tw+2,y+3];if(boxes.some(q=>bx[0]<q[2]&&bx[2]>q[0]&&bx[1]<q[3]&&bx[3]>q[1]))continue;boxes.push(bx);g.lineWidth=3;g.strokeStyle="#050a12";g.strokeText(t,x,y);g.fillStyle=C.ink;g.fillText(t,x,y)}
const nb=row.inconsistent.length,nv=row.inconsistent.filter(x=>x[2]===2).length;
document.getElementById("ffp-"+m).innerHTML=`<b class="num ${nv?"bad":nb?"fib":"ok"}">${nb}</b> of ${f.routes} routes inconsistent${nb?` (${nv} faster than light in vacuum)`:""}${row.moved_max_km>0.5?` · largest correction <b class="num">${fint(row.moved_max_km)} km</b>`:""}`;
}
const one=(f,m,sp)=>{const b=f.one_pair_bound_km[sp];if(!(b.km>0))return"none needed";const [i,j]=b.pair,p=LP.find(q=>(q.a===Math.min(i,j)&&q.b===Math.max(i,j))),U=(sp==="fibre"?CF:CV)*p.T/2000+25,X=dist(m,LS[i],LS[j]);
return`${fint(b.km)} km: ${esc(LS[i].name)} ↔ ${esc(LS[j].name)} are ${fint(X)} km apart on the map; ${fmt(p.T)} ms allows at most ${fint(U)} km`};
document.getElementById("fftab").innerHTML=`<thead><tr><th>Map</th><th>Minimum correction, light in fibre</th><th>Minimum correction, light in vacuum</th><th>Bound from a single pair (no solver), fibre</th></tr></thead><tbody>`+
FFM.map(([m,n,k])=>{const f=F[k];return`<tr><td>${n}</td><td class="num">${fint(f.min_correction_km.fibre)} km</td><td class="num">${fint(f.min_correction_km.vacuum)} km</td><td>${one(f,m,"fibre")}</td></tr>`}).join("")+"</tbody>";
document.getElementById("ffnote").innerHTML=`Routes measured by ${SRC[S.src].label}; ${D.flat.slack_km} km is allowed at each end, as elsewhere on this page. A single pair bounds the correction from below without any solver: its two cities can approach each other by at most twice the correction. The minimum itself is computed with an interior-point solver (cvxpy, Clarabel); the arrangement it returns satisfies every measured pair. Code: ${lnk(SRCV+"src/flatfit.py","flatfit.py")}; results: ${lnk(SRCV+"src/data/flatfit.json","flatfit.json")}.`;
}
document.getElementById("ffb").addEventListener("input",e=>{FFB=+e.target.value;renderFlat()});
/* ---------- method ---------- */ /* ---------- method ---------- */
function renderMethod(){ function renderMethod(){
const best=rows=>rows.reduce((b,r)=>r[1]<b[1]?r:b); const best=rows=>rows.reduce((b,r)=>r[1]<b[1]?r:b);
@@ -1014,7 +972,7 @@ ${vr?`<div>Vultr servers</div><div>Virtual servers rented for this survey in ${v
} }
/* ---------- boot ---------- */ /* ---------- boot ---------- */
function layoutAll(){resize();fitKm();drawSM();renderEx();renderFlat();drawSpeed();frame();viewBtns()} function layoutAll(){resize();fitKm();drawSM();renderEx();drawSpeed();frame();viewBtns()}
let rszT=0;window.addEventListener("resize",()=>{clearTimeout(rszT);rszT=setTimeout(layoutAll,120)}); let rszT=0;window.addEventListener("resize",()=>{clearTimeout(rszT);rszT=setTimeout(layoutAll,120)});
document.querySelectorAll(".models button").forEach(b=>b.classList.toggle("on",b.dataset.v===S.view)); document.querySelectorAll(".models button").forEach(b=>b.classList.toggle("on",b.dataset.v===S.view));
const ia=LS.findIndex(s=>s.name==="Sydney"),ib=LS.findIndex(s=>s.name==="Auckland"); const ia=LS.findIndex(s=>s.name==="Sydney"),ib=LS.findIndex(s=>s.name==="Auckland");
@@ -1024,6 +982,6 @@ document.getElementById("srcsel").value=S.src;
{document.getElementById("eyeb").textContent=`${LS.length} sites · ${Object.keys(D.netlines).length} cables · 4 models`; {document.getElementById("eyeb").textContent=`${LS.length} sites · ${Object.keys(D.netlines).length} cables · 4 models`;
document.getElementById("step1").textContent=`Globalping probes, rented Vultr servers and a residential connection measured round-trip times to RIPE Atlas anchors and to each other across ${LS.length} sites. The minimum for each pair is used.`} document.getElementById("step1").textContent=`Globalping probes, rented Vultr servers and a residential connection measured round-trip times to RIPE Atlas anchors and to each other across ${LS.length} sites. The minimum for each pair is used.`}
legend();renderScore();renderLadder();buildSM();renderX();renderNet();byoInit();renderMethod();renderCode(); legend();renderScore();renderLadder();buildSM();renderX();renderNet();byoInit();renderMethod();renderCode();
readout();renderCard();resize();renderEx();renderFlat();drawSM();drawSpeed();kick();frame(); readout();renderCard();resize();renderEx();drawSM();drawSpeed();kick();frame();
document.fonts&&document.fonts.ready.then(()=>{resize();drawSM();renderEx();renderFlat();drawSpeed();frame();viewBtns()}); document.fonts&&document.fonts.ready.then(()=>{resize();drawSM();renderEx();drawSpeed();frame();viewBtns()});
</script> </script>
+14
View File
@@ -0,0 +1,14 @@
import { chromium } from 'playwright-core';
import { PAGE } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH }); const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errs = []; p.on('pageerror', e => errs.push(e.message));
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1200);
const run = async (loc, anchor, ms) => { await p.fill('#byoA', loc); await p.selectOption('#byoB', { label: anchor }); await p.fill('#byoT', ms); await p.click('#byo button.go'); await p.waitForTimeout(200);
return (await p.innerText('#byoout')).replace(/\s+/g, ' ').slice(0, 330) };
const opts = await p.$$eval('#byoB option', o => o.map(x => x.textContent));
console.log('anchors offered:', opts.length);
console.log('1', await run('Detroit, US', opts.find(o => o.startsWith('Sydney')), '197.4'));
console.log('2', await run('Southfield', opts.find(o => o.startsWith('Auckland')), '222.3'));
console.log('3', await run('-33.87, 151.21', opts.find(o => o.startsWith('Auckland')), '25'));
console.log('4', await run('Nowhereville', opts.find(o => o.startsWith('Sydney')), '20'));
console.log('ERRORS', JSON.stringify(errs)); await b.close();
+7
View File
@@ -0,0 +1,7 @@
import { chromium } from 'playwright-core';
import { PAGE } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH }); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(e.message));
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1000);
await p.focus('#byoA'); const v = await p.inputValue('#byoA');
await p.type('#byoA', 'Lond'); const opts = await p.$$eval('#byocities option', o => o.filter(x => x.value.toLowerCase().startsWith('lond')).map(x => x.value));
console.log('after focus:', JSON.stringify(v), 'matches for "Lond":', opts.slice(0, 4), 'ERRORS', JSON.stringify(errs)); await b.close();
+11
View File
@@ -0,0 +1,11 @@
import { chromium } from 'playwright-core';
import { PAGE } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH }); const errs = [];
for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } }); p.on('pageerror', e => errs.push(e.message));
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1200);
await p.locator('#netx').screenshot({ path: `out/netx_${w}.png` });
const sw = await p.evaluate(() => document.documentElement.scrollWidth); if (sw !== w) errs.push(`${w} scrollWidth ${sw}`);
await p.close();
}
console.log('ERRORS', JSON.stringify(errs)); await b.close();