T-527: the second review's fixes — the zoom-back frame kept by the lane that sees the zoom first, its publish clear of reads in flight; about:addons' search filter re-measured
Daemon: zoomBackKeep(newZoom) from either lane (only a resolution-1 frame not at the new zoom, once per shown frame, reusing its buffer), no publish while a job's read may copy from a slot, the main lane's old-zoom frames superseded, dropped at an input and once the tree is at 1 again; an adopted frame at the held zoom releases the hold; the world offset cleared for a rooted document; the adopted failure count; mLoadWasDone atomic. jihadPersonas.js: the search list's filter attributes observed; a detail view's length kept only in the window made for it. Device: double taps out 0-1 ms with the page's script busy (12 of 12 zooms within 250 ms), about:addons sharp after a pinch, the search filter's length followed, pans and verify unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc7bae02be
commit
31fce6143d
8 files changed
+10375
-21
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -290,3 +290,27 @@ dtap10b-daemon-gpu.log: 20 of 20 taps zoomed (0.6882 <-> 2.3273); the first fram
|
||||
out 0-3 ms (the frame kept at the zoom in), the double taps in 58, 52, 173, 251, 69, 52, 108, 59, 106, 65 ms —
|
||||
19 of 20 within 250 ms, the slowest 251. In the drifting run a zoom in waited 849 ms behind a 926 ms tile-upload
|
||||
transaction of the page's own on the compositor thread (the known class; T-527's floor).
|
||||
|
||||
## The second review (inspector, read-only, 2026-10-26) and its fixes
|
||||
Fourteen findings over 623dc1ac, 286a9265, 180c8f07, 45a63a92 and engine 0064/0065; all fixed but F-013's "never"
|
||||
(now 30 s) and F-014 (made atomic):
|
||||
- F-001 (P2) the kept frame came from the express lane's frame at the new zoom whenever the main thread was busy at
|
||||
the zoom in: now kept by whichever lane sees the zoom first (the express lane before it publishes at the new
|
||||
zoom), and only a frame at a resolution of 1 not already at the new zoom. Device (dtap6-busy, the page's script
|
||||
pumping 1-1.3 s): six double taps out, every one 0-1 ms from the kept frame; all 12 zooms within 250 ms.
|
||||
- F-002 (P2) the kept frame's publish could write a slot a composed job's read still copies from: no publish while
|
||||
an express job or a main-lane job not fully read is in flight.
|
||||
- F-003 (P2, engine 0064) a zoom on a page with no root scroll frame was never repainted at its resolution (the skip
|
||||
saw nothing pending): engine 0066 repaints when the resolution differs from the one painted. Device: about:addons
|
||||
pinched to 1.66, its text sharp.
|
||||
- F-004 (P2) about:addons' search filter (My Add-ons / Available) did not re-measure: the search list's filter
|
||||
attributes and rows observed. Device: My 1540 rows, Available 1202, My 1540 again.
|
||||
- F-005 the kept frame's publish now supersedes the main lane's frames at the old zoom; F-006 the keep reuses its
|
||||
buffer, keeps once per shown frame, and is dropped once the tree is at 1 again with the card at a zoom of 1;
|
||||
F-007 dropped at a click, a key or inserted text; F-008 an adopted frame at the held zoom releases the hold, and
|
||||
only a genuine failure of one counts; F-009 a rooted document after a rootless one clears the world offset;
|
||||
F-010 the keep reads the shown frame through the express lane's snapshot, bounded; F-011 an add-on's details keep
|
||||
their length only while the window is the one made for it; F-012 (engine) the skip asks the child widget too;
|
||||
F-013 (engine) a failed band surface is tried again after 30 s; F-014 mLoadWasDone atomic.
|
||||
Regression: desktop Wikipedia 22.8 fps, the long page 33.3 (sensors running), verify 0 rows wrong in 58 checks
|
||||
(pan-r2-*), default-check on the GPU path with 0 faults.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1381,23 +1381,33 @@ void BrowserPageGoanna::gpuCriticalEnd(const char* why) {
|
||||
mNeedsPaint = true;
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::zoomBackKeep() {
|
||||
void BrowserPageGoanna::zoomBackKeep(double newZoom) {
|
||||
FrameLockGuard frameLock;
|
||||
static const bool sOff = getenv("JIHAD_GPU_NO_ZOOM_BACK") != nullptr;
|
||||
zoomBackDrop();
|
||||
const int s = slotForKey(mShownKey);
|
||||
if (sOff || !mPage || !mPage->GpuPath() || mFrozen || s < 0 || !mShmBuf[s]) return;
|
||||
const BrowserOffscreenInfo* oi = (const BrowserOffscreenInfo*)mShmBuf[s];
|
||||
if (sOff || !mPage || !mPage->GpuPath() || mFrozen || !mShownKey || !mXSnap.ok) return;
|
||||
if (mZoomBack.ok && mZoomBack.pubSeq == mPubSeq) return; // this very frame is kept already
|
||||
// the shown frame from the express lane's snapshot of the segments (attached once, sized without the trailer)
|
||||
const XSnap& S = mXSnap;
|
||||
int s = -1;
|
||||
for (int i = 0; i < 3; ++i) if (S.keys[i] == mShownKey && S.base[i]) s = i;
|
||||
if (s < 0) return;
|
||||
const unsigned char* b = S.base[s];
|
||||
const BrowserOffscreenInfo* oi = (const BrowserOffscreenInfo*)b;
|
||||
const double zs = oi->contentZoom;
|
||||
// the frame before the zoom in: at a resolution of 1 and not already at the new zoom (a busy main thread
|
||||
// handles the zoom after the express lane has published at it: that frame is not the one to go back to)
|
||||
if (!(zs > 0.0) || fabs(zs - newZoom) < 1e-4 || GoannaRenderPage::GpuResolutionFor(zs) > 1.0005f) return;
|
||||
const int w = oi->bufferWidth, h = oi->bufferHeight;
|
||||
JihadFrameTrailer* t = shmTrailer(mShownKey);
|
||||
const int win = (t && t->magic == kJihadWindowMagic) ? (int)t->windowRows : 0;
|
||||
const size_t need = sizeof(BrowserOffscreenInfo) + ((size_t)win + h) * w * 4;
|
||||
if (w <= 0 || h <= 0 || w > 8192 || h > 8192 || win < 0 || need > mShmSize[s] || !(oi->contentZoom > 0.0)) return;
|
||||
const unsigned char* src = mShmBuf[s] + sizeof(BrowserOffscreenInfo) + (size_t)win * w * 4;
|
||||
mZoomBackPx.assign(src, src + (size_t)w * h * 4);
|
||||
if (w <= 0 || h <= 0 || w > 8192 || h > 8192 || S.seg[s] <= S.hdr) return;
|
||||
const size_t rowBytes = (size_t)w * 4, rows = (S.seg[s] - S.hdr) / rowBytes;
|
||||
const JihadFrameTrailer* t = jihadFrameTrailer(const_cast<unsigned char*>(b), S.seg[s] + kJihadTrailerBytes);
|
||||
const long win = (t && t->magic == kJihadWindowMagic) ? (long)t->windowRows : 0;
|
||||
if ((size_t)h > rows || win < 0 || (size_t)win > rows - (size_t)h) return;
|
||||
const unsigned char* src = b + S.hdr + (size_t)win * rowBytes;
|
||||
mZoomBackPx.assign(src, src + (size_t)h * rowBytes); // the capacity kept from the last keep
|
||||
ZoomBack& zb = mZoomBack;
|
||||
zb.zoom = oi->contentZoom; zb.x = oi->renderedX; zb.y = oi->renderedY; zb.w = w; zb.h = h; zb.contentH = mLastContentH;
|
||||
zb.ok = true;
|
||||
zb.zoom = zs; zb.x = oi->renderedX; zb.y = oi->renderedY; zb.w = w; zb.h = h; zb.contentH = mLastContentH;
|
||||
zb.pubSeq = mPubSeq; zb.ok = true;
|
||||
}
|
||||
|
||||
bool BrowserPageGoanna::zoomBackPublish(int64_t now, double Z, const XWant& want) {
|
||||
@@ -1410,6 +1420,10 @@ bool BrowserPageGoanna::zoomBackPublish(int64_t now, double Z, const XWant& want
|
||||
zoomBackDrop();
|
||||
return false;
|
||||
}
|
||||
// nothing in flight on the compositor or read thread: a composed job's read copies from another slot (its
|
||||
// base) after it reports composed, and that slot is no job's own (review 2026-10-26)
|
||||
if (mXJob.on) return false;
|
||||
for (const GpuJob& gj : mGpuJobs) if (mPage->ComposePeek(gj.token) != 1) return false;
|
||||
// a slot: not the one the card shows, not out with the card, not a lane's
|
||||
const size_t need = S.hdr + mZoomBackPx.size();
|
||||
int slot = -1;
|
||||
@@ -1439,6 +1453,8 @@ bool BrowserPageGoanna::zoomBackPublish(int64_t now, double Z, const XWant& want
|
||||
mSink.msgPainted(key); noteShown(key);
|
||||
mInFlight[slot] = true; mPaintMs[slot] = now;
|
||||
if (!mXMainWindow && mActiveKey == key) mActiveKey = keyAfter(key);
|
||||
// the newest frame, as an express publish is: the main lane's jobs at the old zoom are dropped at their poll
|
||||
++mXFrames; mXLastZoom = Z; mXLastXPubMs = now;
|
||||
JIHAD_GPU_LOG("[jihad-gpu] zoom %.4f: the frame kept at the zoom in published again, %ld ms after the zoom\n", Z, (long)(now - want.ms));
|
||||
// device rounds: JIHAD_GPU_ZOOM_BACK_DUMP=<path>, the frame as the adapter reads it (3.7 MB: delete it at once)
|
||||
static const char* sBackDump = getenv("JIHAD_GPU_ZOOM_BACK_DUMP");
|
||||
@@ -1621,7 +1637,7 @@ void BrowserPageGoanna::setZoomAndScroll(double zoom, int x, int y) {
|
||||
static const bool sNoZoomHold = getenv("JIHAD_GPU_NO_ZOOM_HOLD") != nullptr;
|
||||
if (z != mGpuZoom) {
|
||||
// T-527 (R6): the frame at this zoom, for a zoom back to it (zoomBackPublish)
|
||||
if (jihadExpressOn() && mPage->GpuResolutionNow() <= 1.0005f && GoannaRenderPage::GpuResolutionFor(z) > 1.0005f) zoomBackKeep();
|
||||
if (jihadExpressOn() && mPage->GpuResolutionNow() <= 1.0005f && GoannaRenderPage::GpuResolutionFor(z) > 1.0005f) zoomBackKeep(z);
|
||||
if (!sNoZoomHold && mPage->GpuDisplayPortOk()) {
|
||||
// T-504: the painted tiles first, at the new zoom (gpuScrollTo keeps the engine where it is)
|
||||
mGpuZoom = z; mGpuZoomHoldMs = jihadNowMs(); mGpuZoomHoldZ = z;
|
||||
@@ -1853,6 +1869,7 @@ void BrowserPageGoanna::getHistoryState(bool* back, bool* fwd) {
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::clickAt(int x, int y, int numClicks) {
|
||||
{ FrameLockGuard frameLock; if (mZoomBack.ok) zoomBackDrop(); } // T-527 (review): the kept frame is from before this input
|
||||
if (!mPage) return;
|
||||
fprintf(stderr, "[jihad-bs] clickAt x=%d y=%d n=%d\n", x, y, numClicks);
|
||||
// The adapter already sends CONTENT coords (asyncCmdClickAt contentX/contentY =
|
||||
@@ -1870,6 +1887,7 @@ void BrowserPageGoanna::clickAt(int x, int y, int numClicks) {
|
||||
mNeedsPaint = true;
|
||||
}
|
||||
void BrowserPageGoanna::keyDown(int key, int modifiers, int chr) {
|
||||
{ FrameLockGuard frameLock; if (mZoomBack.ok) zoomBackDrop(); } // T-527 (review): the kept frame is from before this input
|
||||
if (!mPage) return;
|
||||
// T-131 instrument (cavekit-preferences-ui.md R6), same JIHAD_LOG_INSERT gate as InsertText's.
|
||||
// WARRANTED because of an asymmetry that would otherwise void the run: a VKB keystroke arriving
|
||||
@@ -2493,6 +2511,7 @@ bool BrowserPageGoanna::runPageCommand(const char* cmd, const std::string& extra
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::insertStringAtCursor(const char* text) {
|
||||
{ FrameLockGuard frameLock; if (mZoomBack.ok) zoomBackDrop(); } // T-527 (review): the kept frame is from before this input
|
||||
// NB: never log `text` — it is user keystrokes (incl. passwords) and this stream is redirected
|
||||
// to a persistent, user-readable file on device (Jihad review F-163).
|
||||
if (mPage && text) { mPage->InsertText(text); mNeedsPaint = true; if (mNeedsPaint) maybePaint(); }
|
||||
@@ -2960,7 +2979,12 @@ bool BrowserPageGoanna::emitGeometry() {
|
||||
const uint64_t docKey = mPage->DocumentKey();
|
||||
if (docKey != mLayoutDocKey) {
|
||||
mLayoutDocKey = docKey; mLayoutFollowed = false;
|
||||
{ FrameLockGuard frameLock; mGpuRootless = mPage->GpuRootless(); } // T-527: the express lane's offsets from the document's first frame
|
||||
{
|
||||
FrameLockGuard frameLock;
|
||||
mGpuRootless = mPage->GpuRootless(); // T-527: the express lane's offsets from the document's first frame
|
||||
// (review 2026-10-26) a pan while the old rootless document was still shown set the world offset again
|
||||
if (!mGpuRootless && mPage->GpuPath() && (mGpuViewRX != 0.0 || mGpuViewRY != 0.0)) gpuSetView(mGpuZoom > 0.0 ? mGpuZoom : 1.0, 0.0, 0.0);
|
||||
}
|
||||
// the height too: a XUL page's is its list's length (below), and the next page was laid out in a
|
||||
// 768x4000 window (innerHeight, 100vh and a bottom:0 bar at 4000: review 2026-10-25)
|
||||
if (mPage->LayoutWidth() != mPage->Width() || mPage->LayoutHeight() != mPage->Height()) {
|
||||
@@ -3821,7 +3845,8 @@ void BrowserPageGoanna::maybePaint() {
|
||||
}
|
||||
// T-515: an express frame at the held zoom is out (the main lane's own may be dropped for as long
|
||||
// as the page's script runs): the displayport and the anchor follow the zoom now
|
||||
if (mGpuZoomHoldMs && mXLastZoom > 0.0 && fabs(mXLastZoom - mGpuZoomHoldZ) < 1e-9)
|
||||
if (mGpuZoomHoldMs && ((mXLastZoom > 0.0 && fabs(mXLastZoom - mGpuZoomHoldZ) < 1e-9) ||
|
||||
(mXAdoptedZoom > 0.0 && fabs(mXAdoptedZoom - mGpuZoomHoldZ) < 1e-9)))
|
||||
gpuZoomRelease("the first frame at the new zoom is out (the express lane)");
|
||||
// T-527: or posted: the compositor thread composes it on the tree as it stands (the paint at the new
|
||||
// resolution reaches the compositor after it), so that paint can run while it is composed
|
||||
@@ -4522,6 +4547,9 @@ bool BrowserPageGoanna::expressStep(int64_t now) {
|
||||
{ std::lock_guard<std::mutex> l(jihadWantLock()); want = mXWant; waitSince = mXWaitSinceMs; }
|
||||
if (!want.seq) return false;
|
||||
const double Z = (want.zoom >= 0.99 && want.zoom <= 1.01) ? 1.0 : want.zoom; // the paint path's rule
|
||||
// T-527 (R6): a zoom in that will raise the resolution, seen before the main thread handles it: the frame on
|
||||
// the card is kept for the zoom back, before this lane publishes at the new zoom (review 2026-10-26)
|
||||
if (mXTreeRes <= 1.0005 && GoannaRenderPage::GpuResolutionFor(Z) > 1.0005f) zoomBackKeep(Z);
|
||||
static const long sAway = getenv("JIHAD_GPU_EXPRESS_AWAY_MS") ? atol(getenv("JIHAD_GPU_EXPRESS_AWAY_MS")) : 15;
|
||||
// T-527: a main-lane frame composed while the main thread is away in the engine waits for it to
|
||||
// come back and poll. After a double tap on desktop Wikipedia the repaint at the new resolution was
|
||||
@@ -4843,7 +4871,7 @@ bool BrowserPageGoanna::xPoll(int64_t now) {
|
||||
gpuSlotsLost(); // T-517: its change is in no slot's record
|
||||
// T-527 (review): an adopted main-lane job's failure is the main lane's (its count of three gives the
|
||||
// GPU path up at its next failure, on the main thread)
|
||||
if (adopted) ++mGpuRefusals;
|
||||
if (adopted) { if (genOk) ++mGpuRefusals; }
|
||||
else if (genOk && ++mXFailures >= 3) fprintf(stderr, "[jihad-gpu] express lane off for this page: three frames failed\n");
|
||||
return true;
|
||||
}
|
||||
@@ -4924,7 +4952,7 @@ bool BrowserPageGoanna::xPoll(int64_t now) {
|
||||
// T-527 (review): a main-lane frame: the main lane's bookkeeping, as its poll would have done it (the
|
||||
// express lane's would hold the main lane's next frame up to 300 ms for a zoom it is newer than, and
|
||||
// drop the main lane's second job, built on this one)
|
||||
mXMainPubMs = now; mXLastXPubMs = 0; mXCmdPub = mXJob.cmdDone;
|
||||
mXMainPubMs = now; mXLastXPubMs = 0; mXCmdPub = mXJob.cmdDone; mXAdoptedZoom = mXJob.zoom;
|
||||
std::lock_guard<std::mutex> l(jihadWantLock());
|
||||
if (!mXWant.seq || mXCmdPub >= mXWant.cmdSeq) mXWaitSinceMs = 0;
|
||||
} else {
|
||||
@@ -5697,6 +5725,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
{ FrameLockRelease unlocked; mPage->PaintLayers(); jihadTimelineMain("layer paint (the express lane leads)", (long)(jihadNowMs() - now)); }
|
||||
if (mGpuCriticalMs && !mGpuCriticalPaintMs) mGpuCriticalPaintMs = jihadNowMs(); // T-527
|
||||
mXTreeRes = mPage->GpuResolutionNow(); // T-527
|
||||
if (mXTreeRes <= 1.0005 && GoannaRenderPage::GpuResolutionFor(mZoom) <= 1.0005f && mZoomBack.ok) zoomBackDrop(); // (review)
|
||||
mXMainWindow = false; mXMainSlot = -1;
|
||||
}
|
||||
mNeedsPaint = true; // its own frame once the card stops
|
||||
@@ -5713,6 +5742,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
{ const int64_t p0 = jihadNowMs(); FrameLockRelease unlocked; painted = mPage->PaintLayers(); jihadTimelineMain("layer paint", (long)(jihadNowMs() - p0)); }
|
||||
if (mGpuCriticalMs && !mGpuCriticalPaintMs) mGpuCriticalPaintMs = jihadNowMs(); // T-527
|
||||
mXTreeRes = mPage->GpuResolutionNow(); // T-527
|
||||
if (mXTreeRes <= 1.0005 && GoannaRenderPage::GpuResolutionFor(mZoom) <= 1.0005f && mZoomBack.ok) zoomBackDrop(); // (review)
|
||||
mXMainWindow = false; mXMainSlot = -1;
|
||||
if (mXJob.on) { mNeedsPaint = true; return; } // its frame went first: this one on the next tick
|
||||
mGpuPainted = painted;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define JIHAD_BROWSERPAGEGOANNA_H
|
||||
|
||||
#include "JihadFrameWindow.h" // T-517: the frame window trailer (render/browserserver/Src)
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -404,7 +405,7 @@ private:
|
||||
// window build a run; once it is long enough, Backspace deletes a word at a time (not a char).
|
||||
int64_t mLastBackspaceMs;
|
||||
int mBackspaceRun;
|
||||
bool mLoadWasDone;
|
||||
std::atomic<bool> mLoadWasDone; // (review 2026-10-26: the express lane reads it, the pump writes it)
|
||||
int mLastProgress; // last msgLoadProgress value emitted this load (0..99); reset at
|
||||
// each load start so a stale high value can't freeze the bar
|
||||
bool mWatchdogDismissed; // the stall watchdog cleared the overlay while the engine load
|
||||
@@ -551,12 +552,17 @@ private:
|
||||
// at once when the card comes back to exactly that zoom (a double tap out to the fit zoom: the old tiles
|
||||
// drawn at under half their size make no frame, and the paint at the new resolution came 573-669 ms
|
||||
// after the tap). Only while it covers the card's view and the page's height is the same; once.
|
||||
struct ZoomBack { bool ok = false; double zoom = 0.0; int x = 0, y = 0, w = 0, h = 0; long contentH = 0; };
|
||||
// Kept by whichever lane sees the zoom first (the express lane publishes at the new zoom before a busy
|
||||
// main thread handles the command), and only a frame at a resolution of 1 that is not at the new zoom;
|
||||
// dropped at an input (it would show the page from before it), once the tree is at 1 again, and on a new
|
||||
// document, size or freeze (review 2026-10-26).
|
||||
struct ZoomBack { bool ok = false; double zoom = 0.0; int x = 0, y = 0, w = 0, h = 0; long contentH = 0; uint32_t pubSeq = 0; };
|
||||
ZoomBack mZoomBack;
|
||||
std::vector<unsigned char> mZoomBackPx;
|
||||
void zoomBackKeep(); // the main thread (the frame lock)
|
||||
void zoomBackKeep(double newZoom); // either lane (the frame lock)
|
||||
bool zoomBackPublish(int64_t now, double Z, const XWant& want); // the express lane (the frame lock)
|
||||
void zoomBackDrop() { mZoomBack.ok = false; std::vector<unsigned char>().swap(mZoomBackPx); }
|
||||
double mXAdoptedZoom = 0.0; // the zoom of the last main-lane frame the express lane published for it (the hold's release)
|
||||
uint32_t mXAdopted = 0; // T-527: main-lane frames the express lane published for it
|
||||
int64_t mXLastXPubMs = 0; // the express lane's last publish, while it is the newest frame (0: the main lane's is)
|
||||
int mXConsumed[3] = {0, 0, 0}; // returns the express hook applied; returnBuffer skips as many
|
||||
|
||||
@@ -1519,7 +1519,10 @@ function growToList(win) {
|
||||
let viewKey = "";
|
||||
try { viewKey = ADDONS.currentViewId(win) || ""; } catch (e) {}
|
||||
if (panel && natural === 0) {
|
||||
// the length kept only while the window is the one made for it: measured in a smaller window (a
|
||||
// rotation, preferences that collapsed) it is measured afresh (review 2026-10-26)
|
||||
let prev = root.getAttribute("jihad-content-view") === viewKey ? (parseInt(root.getAttribute("jihad-content-height"), 10) || 0) : 0;
|
||||
if (prev > 0 && win.innerHeight + 1 < prev) { prev = 0; }
|
||||
natural = Math.max(need, prev);
|
||||
}
|
||||
if (root.getAttribute("jihad-content-view") !== viewKey) { root.setAttribute("jihad-content-view", viewKey); }
|
||||
@@ -1657,6 +1660,16 @@ function attachPane(win) {
|
||||
pending = later(30, function() { pending = 0; again(); });
|
||||
}).observe(list, {childList: true});
|
||||
}
|
||||
// The search results' filter (My Add-ons / Available Add-ons) shows other rows without a view change or a
|
||||
// new row: their length is measured again when the list's filter attributes or rows change (review 2026-10-26)
|
||||
let slist = ADDONS.element(doc, "searchList");
|
||||
if (slist) {
|
||||
let sizing2 = 0;
|
||||
new win.MutationObserver(function() {
|
||||
if (sizing2) { return; }
|
||||
sizing2 = later(100, function() { sizing2 = 0; try { growToList(win); } catch (e) { log("about:addons height: " + e); } });
|
||||
}).observe(slist, {childList: true, attributes: true, attributeFilter: ["local", "remote", "hidden"]});
|
||||
}
|
||||
// An add-on's preferences arrive in its page after the view has changed (the manager loads
|
||||
// them): the page's height is measured again when they do.
|
||||
let rows = ADDONS.element(doc, "detailRows");
|
||||
|
||||
Reference in new issue
Block a user