Skip to content

Commit 2a467d2

Browse files
committed
feat(profile): capability-based auto profile; keep legacy mappings
1 parent 1d92b2c commit 2a467d2

File tree

1 file changed

+173
-41
lines changed

1 file changed

+173
-41
lines changed

plugins/profile/default-policy.js

Lines changed: 173 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,176 @@
11
/**
22
* Default Profile Policy Plugin
3-
* Automatically detects device type and sets appropriate performance profiles
4-
* Provides device profile configuration for AR processing
3+
* Automatically computes a capability-based device profile at runtime.
4+
* Backward-compatible: legacy DEVICE_PROFILES mappings still supported.
55
*/
66

7-
import { RESOURCES, DEVICE_PROFILES } from "../../src/core/components.js";
7+
import { RESOURCES, DEVICE_PROFILES, QUALITY_TIERS } from "../../src/core/components.js";
88

99
export const defaultProfilePlugin = {
1010
id: "profile:default",
11-
name: "Default Profile Policy",
11+
name: "Default Profile Policy (auto)",
1212
type: "profile",
1313

1414
/**
15-
* Initialize the plugin
15+
* Initialize the plugin: compute auto profile and publish it
1616
*/
1717
async init(context) {
18-
// Detect device profile and set it as a resource
19-
const profile = this.detectProfile();
18+
const profile = await this._computeAutoProfile();
2019
context.ecs.setResource(RESOURCES.DEVICE_PROFILE, profile);
20+
// Emit a profile-applied event for observers (optional)
21+
context?.eventBus?.emit?.("profile:applied", { profile });
2122
},
2223

2324
/**
24-
* Detect the appropriate device profile based on user agent and hardware
25-
* @returns {Object} Device profile configuration
25+
* Preferred: detect a structured capability-based profile
2626
*/
27-
detectProfile() {
28-
const isMobile = this._isMobileDevice();
29-
const profileLabel = isMobile ? "phone-normal" : "desktop-normal";
27+
async detectProfile() {
28+
return this._computeAutoProfile();
29+
},
30+
31+
/**
32+
* Compute a capability-based profile
33+
* Returns a structured profile with backward-compatible fields.
34+
*/
35+
async _computeAutoProfile() {
36+
const caps = this._getCaps();
37+
const bench = await this._microBenchmark(8); // ~8ms probe
38+
const score = this._scoreCaps(caps, bench);
39+
const tierInfo = this._pickTier(score);
40+
const [w, h] = tierInfo.capture;
41+
42+
// Backward-compat top-level fields used in older examples:
43+
const legacyCompatible = {
44+
label: `auto-${tierInfo.tier}`,
45+
sourceWidth: w,
46+
sourceHeight: h,
47+
displayWidth: w,
48+
displayHeight: h,
49+
canvasWidth: w,
50+
canvasHeight: h,
51+
maxDetectionRate: 60,
52+
};
53+
54+
// New structured fields:
55+
const structured = {
56+
qualityTier: tierInfo.tier, // QUALITY_TIERS value
57+
score,
58+
caps,
59+
capture: {
60+
sourceWidth: w,
61+
sourceHeight: h,
62+
displayWidth: w,
63+
displayHeight: h,
64+
fpsHint: 30,
65+
},
66+
processing: {
67+
budgetMsPerFrame: tierInfo.budget,
68+
complexity: tierInfo.complexity,
69+
},
70+
};
71+
72+
return { ...legacyCompatible, ...structured };
73+
},
74+
75+
/**
76+
* Get device capability signals (defensive checks for non-browser envs)
77+
*/
78+
_getCaps() {
79+
const nav = typeof navigator !== "undefined" ? navigator : {};
80+
const win = typeof window !== "undefined" ? window : {};
81+
const scr = typeof screen !== "undefined" ? screen : {};
82+
83+
const userAgentHint = typeof nav.userAgent === "string" ? nav.userAgent : "";
84+
const cores = Math.max(1, Number(nav.hardwareConcurrency || 2));
85+
const memoryGB = Math.max(0.5, Number(nav.deviceMemory || 2));
86+
const webgl2 = !!win.WebGL2RenderingContext;
87+
const wasmSIMD = typeof WebAssembly === "object" && typeof WebAssembly.validate === "function";
88+
const screenLongSide = Math.max(Number(scr.width || 0), Number(scr.height || 0)) || 0;
89+
90+
let torch = false;
91+
let focusMode = "unknown";
92+
try {
93+
const getSC = nav.mediaDevices?.getSupportedConstraints?.bind(nav.mediaDevices);
94+
const sc = getSC ? getSC() : {};
95+
torch = !!sc?.torch;
96+
focusMode = sc?.focusMode ? "supported" : "unknown";
97+
} catch {
98+
// ignore
99+
}
100+
101+
return {
102+
userAgentHint,
103+
cores,
104+
memoryGB,
105+
webgl2,
106+
wasmSIMD,
107+
screenLongSide,
108+
camera: { torch, focusMode },
109+
};
110+
},
111+
112+
/**
113+
* Very small CPU probe to approximate budget
114+
*/
115+
async _microBenchmark(msTarget = 8) {
116+
if (typeof performance === "undefined" || typeof performance.now !== "function") {
117+
return 0;
118+
}
119+
const start = performance.now();
120+
let acc = 0;
121+
while (performance.now() - start < msTarget) {
122+
// Cheap floating math
123+
for (let i = 0; i < 1000; i++) acc += Math.sqrt(i + (acc % 5));
124+
// Safety: don't run too long if timers behave oddly
125+
if (performance.now() - start > msTarget * 2) break;
126+
}
127+
return acc;
128+
},
30129

31-
return this.getProfile(profileLabel);
130+
/**
131+
* Convert caps + bench signal into a 0..100 score
132+
*/
133+
_scoreCaps(caps, benchSignal) {
134+
let score = 0;
135+
// Cores: up to 6 cores -> 30 pts
136+
score += Math.min(30, (caps.cores || 0) * 5);
137+
// Memory: up to ~7.5 GB -> 30 pts
138+
score += Math.min(30, (caps.memoryGB || 0) * 4);
139+
// WebGL2: 10 pts
140+
if (caps.webgl2) score += 10;
141+
// WASM SIMD hint: 10 pts
142+
if (caps.wasmSIMD) score += 10;
143+
// Screen long side: up to ~6000 px -> 10 pts (rough indicator of class)
144+
score += Math.min(10, Math.floor((caps.screenLongSide || 0) / 600));
145+
146+
// Normalize bench signal into ~0..10
147+
if (typeof benchSignal === "number") {
148+
const norm = Math.max(0, Math.log10(Math.max(10, benchSignal)));
149+
score += Math.min(10, 5 + norm);
150+
}
151+
152+
score = Math.round(Math.max(0, Math.min(100, score)));
153+
return score;
154+
},
155+
156+
/**
157+
* Map score to a quality tier and capture/budget hints
158+
*/
159+
_pickTier(score) {
160+
if (score >= 85) {
161+
return { tier: QUALITY_TIERS.ULTRA, capture: [1280, 720], budget: 12, complexity: "high" };
162+
}
163+
if (score >= 65) {
164+
return { tier: QUALITY_TIERS.HIGH, capture: [960, 540], budget: 10, complexity: "high" };
165+
}
166+
if (score >= 45) {
167+
return { tier: QUALITY_TIERS.MEDIUM, capture: [800, 450], budget: 8, complexity: "medium" };
168+
}
169+
return { tier: QUALITY_TIERS.LOW, capture: [640, 360], budget: 6, complexity: "low" };
32170
},
33171

34172
/**
35-
* Get profile configuration by label
36-
* @param {string} label - Profile label
37-
* @returns {Object} Profile configuration
173+
* Legacy mapping: return a minimal legacy profile by label
38174
*/
39175
getProfile(label) {
40176
const profiles = {
@@ -76,45 +212,41 @@ export const defaultProfilePlugin = {
76212
},
77213

78214
/**
79-
* Check if the current device is a mobile device
80-
* @private
81-
*/
82-
_isMobileDevice() {
83-
const userAgent = navigator.userAgent;
84-
return !!(
85-
userAgent.match(/Android/i) ||
86-
userAgent.match(/webOS/i) ||
87-
userAgent.match(/iPhone/i) ||
88-
userAgent.match(/iPad/i) ||
89-
userAgent.match(/iPod/i) ||
90-
userAgent.match(/BlackBerry/i) ||
91-
userAgent.match(/Windows Phone/i)
92-
);
93-
},
94-
95-
/**
96-
* Set a specific profile
97-
* @param {string} label - Profile label
98-
* @param {Object} context - Engine context
215+
* Keep legacy setter: If a legacy label is passed, set that profile.
99216
*/
100217
setProfile(label, context) {
101218
const profile = this.getProfile(label);
102219
context.ecs.setResource(RESOURCES.DEVICE_PROFILE, profile);
103220
},
104221

105222
/**
106-
* Get the current profile
107-
* @param {Object} context - Engine context
108-
* @returns {Object} Current device profile
223+
* Read currently applied profile
109224
*/
110225
getCurrentProfile(context) {
111226
return context.ecs.getResource(RESOURCES.DEVICE_PROFILE);
112227
},
113228

114229
/**
115-
* Dispose the plugin
230+
* Legacy mobile detection retained (unused by default)
231+
* @private
232+
*/
233+
_isMobileDevice() {
234+
const ua = (typeof navigator !== "undefined" && navigator.userAgent) || "";
235+
return !!(
236+
ua.match(/Android/i) ||
237+
ua.match(/webOS/i) ||
238+
ua.match(/iPhone/i) ||
239+
ua.match(/iPad/i) ||
240+
ua.match(/iPod/i) ||
241+
ua.match(/BlackBerry/i) ||
242+
ua.match(/Windows Phone/i)
243+
);
244+
},
245+
246+
/**
247+
* Dispose hook
116248
*/
117249
async dispose() {
118250
// Nothing to clean up
119251
},
120-
};
252+
};

0 commit comments

Comments
 (0)