Skip to content

Commit

Permalink
Merge pull request #561 from microsoft/samart/styleSpeed
Browse files Browse the repository at this point in the history
AdoptedStyles and Slot fixes
  • Loading branch information
ender336 authored Mar 26, 2024
2 parents d4d4de3 + 2d15f1e commit 235b9b1
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 108 deletions.
4 changes: 2 additions & 2 deletions packages/clarity-decode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-decode",
"version": "0.7.25",
"version": "0.7.26",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand All @@ -26,7 +26,7 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-js": "^0.7.25"
"clarity-js": "^0.7.26"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/clarity-devtools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-devtools",
"version": "0.7.25",
"version": "0.7.26",
"private": true,
"description": "Adds Clarity debugging support to browser devtools",
"author": "Microsoft Corp.",
Expand All @@ -24,9 +24,9 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-decode": "^0.7.25",
"clarity-js": "^0.7.25",
"clarity-visualize": "^0.7.25"
"clarity-decode": "^0.7.26",
"clarity-js": "^0.7.26",
"clarity-visualize": "^0.7.26"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/clarity-devtools/static/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"manifest_version": 2,
"name": "Microsoft Clarity Developer Tools",
"description": "Clarity helps you understand how users are interacting with your website.",
"version": "0.7.25",
"version_name": "0.7.25",
"version": "0.7.26",
"version_name": "0.7.26",
"minimum_chrome_version": "50",
"devtools_page": "devtools.html",
"icons": {
Expand Down
2 changes: 1 addition & 1 deletion packages/clarity-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-js",
"version": "0.7.25",
"version": "0.7.26",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/clarity-js/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default [
plugins: [
alias({
entries: [
{ find: '@src/layout/style', replacement: '@src/insight/blank' }
{ find: '@src/layout/style', replacement: '@src/insight/style' }
]
}),
resolve(),
Expand Down
2 changes: 1 addition & 1 deletion packages/clarity-js/src/core/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
let version = "0.7.25";
let version = "0.7.26";
export default version;
52 changes: 52 additions & 0 deletions packages/clarity-js/src/insight/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Metric } from "@clarity-types/data";
import { StyleSheetState } from "@clarity-types/layout";
import * as core from "@src/core";
import * as metric from "@src/data/metric";

export let state: StyleSheetState[] = [];
let replace: (text?: string) => Promise<CSSStyleSheet> = null;
let replaceSync: (text?: string) => void = null;

export function start(): void {
reset();

if (replace === null) {
replace = CSSStyleSheet.prototype.replace;
CSSStyleSheet.prototype.replace = function(): Promise<CSSStyleSheet> {
if (core.active()) {
metric.max(Metric.ConstructedStyles, 1);
}
return replace.apply(this, arguments);
};
}

if (replaceSync === null) {
replaceSync = CSSStyleSheet.prototype.replaceSync;
CSSStyleSheet.prototype.replaceSync = function(): void {
if (core.active()) {
metric.max(Metric.ConstructedStyles, 1);
}
return replaceSync.apply(this, arguments);
};
}
}

export function checkDocumentStyles(documentNode: Document): void {
if (!documentNode?.adoptedStyleSheets) {
// if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
return;
}
metric.max(Metric.ConstructedStyles, 1);
}

export function compute(): void {
checkDocumentStyles(document);
}

export function reset(): void {
state = [];
}

export function stop(): void {
reset();
}
6 changes: 0 additions & 6 deletions packages/clarity-js/src/layout/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ export default function (node: Node, source: Source): Node {
// In some cases, external libraries like vue-fragment, can modify parentNode property to not be in sync with the DOM
// For correctness, we first look at parentElement and if it not present then fall back to using parentNode
parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode as HTMLElement : null);
// For HTML slots, the parentElement doesn't actually match what is rendered. If we have an assignedSlot, we use that
// as the parent to ensure our visualizations match the browser behavior for end users.
var slottedParent = (node as HTMLElement).assignedSlot;
if (slottedParent) {
parent = slottedParent;
}
// If we encounter a node that is part of SVG namespace, prefix the tag with SVG_PREFIX
if (element.namespaceURI === Constant.SvgNamespace) { tag = Constant.SvgPrefix + tag; }

Expand Down
42 changes: 26 additions & 16 deletions packages/clarity-js/src/layout/style.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Event } from "@clarity-types/data";
import { Event, Metric } from "@clarity-types/data";
import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
import { time } from "@src/core/time";
import { shortid } from "@src/data/metadata";
import { shortid, data as metadataFields } from "@src/data/metadata";
import encode from "@src/layout/encode";
import { getId, getNode } from "@src/layout/dom";
import * as core from "@src/core";
import { getCssRules } from "./node";
import * as metric from "@src/data/metric";

export let state: StyleSheetState[] = [];
let replace: (text?: string) => Promise<CSSStyleSheet> = null;
let replaceSync: (text?: string) => void = null;
const styleSheetId = 'claritySheetId';
const styleSheetPageNum = 'claritySheetNum';
let styleSheetMap = {};

export function start(): void {
Expand All @@ -20,12 +22,8 @@ export function start(): void {
replace = CSSStyleSheet.prototype.replace;
CSSStyleSheet.prototype.replace = function(): Promise<CSSStyleSheet> {
if (core.active()) {
if (!this[styleSheetId]) {
this[styleSheetId] = shortid();
// need to pass a create style sheet event (don't add it to any nodes, but do create it)
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Create);
}

metric.max(Metric.ConstructedStyles, 1);
bootStrapStyleSheet(this);
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]);
}
return replace.apply(this, arguments);
Expand All @@ -36,27 +34,40 @@ export function start(): void {
replaceSync = CSSStyleSheet.prototype.replaceSync;
CSSStyleSheet.prototype.replaceSync = function(): void {
if (core.active()) {
if (!this[styleSheetId]) {
this[styleSheetId] = shortid();
// need to pass a create style sheet event (don't add it to any nodes, but do create it)
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Create);
}
metric.max(Metric.ConstructedStyles, 1);
bootStrapStyleSheet(this);
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]);
}
return replaceSync.apply(this, arguments);
};
}
}

function bootStrapStyleSheet(styleSheet: CSSStyleSheet): void {
// If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
// For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
// on the previously created style sheet for page N-1.
const pageNum = metadataFields.pageNum;
if (styleSheet[styleSheetPageNum] !== pageNum) {
styleSheet[styleSheetPageNum] = pageNum;
styleSheet[styleSheetId] = shortid();
// need to pass a create style sheet event (don't add it to any nodes, but do create it)
trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.Create);
}
}

export function checkDocumentStyles(documentNode: Document): void {
if (!documentNode?.adoptedStyleSheets) {
// if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
return;
}
metric.max(Metric.ConstructedStyles, 1);
let currentStyleSheets: string[] = [];
for (var styleSheet of documentNode.adoptedStyleSheets) {
// if we haven't seen this style sheet, create it and pass a replaceSync with its contents
if (!styleSheet[styleSheetId]) {
const pageNum = metadataFields.pageNum;
// if we haven't seen this style sheet, create it and call replaceSync with its contents to bootstrap it
if (styleSheet[styleSheetPageNum] !== pageNum) {
styleSheet[styleSheetPageNum] = pageNum;
styleSheet[styleSheetId] = shortid();
trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.Create);
trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.ReplaceSync, getCssRules(styleSheet));
Expand All @@ -82,7 +93,6 @@ export function compute(): void {

export function reset(): void {
state = [];

}

export function stop(): void {
Expand Down
56 changes: 20 additions & 36 deletions packages/clarity-js/src/layout/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,25 @@ import * as task from "@src/core/task";
import node from "@src/layout/node";

export default async function(root: Node, timer: Timer, source: Source): Promise<void> {
let primaryQueue = [root];
let slottedNodesQueue: Node[] = [];
for (var currentQueue of [primaryQueue, slottedNodesQueue]) {
while (currentQueue.length > 0) {
let entry = currentQueue.shift();
let next = entry.firstChild;

while (next) {
// During traversal there is not a guarantee that the assigned child here is being found after the slot to which it is placed
// as the typical parent/child methods don't reflect what is rendered. We need to make sure all other mutations and
// discoveries are processed before we process a slotted element. Once we are processing the slottedNodesQueue we can
// skip this effort and go directly to our breadth first traversal.
if (currentQueue == primaryQueue) {
var slottedParent = (next as HTMLElement).assignedSlot;
if (slottedParent) {
slottedNodesQueue.push(next);
} else {
currentQueue.push(next);
}
} else {
currentQueue.push(next);
}
next = next.nextSibling;
}

// Check the status of current task to see if we should yield before continuing
let state = task.state(timer);
if (state === Task.Wait) { state = await task.suspend(timer); }
if (state === Task.Stop) { break; }

// Check if processing a node gives us a pointer to one of its sub nodes for traversal
// E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set
// Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe.
let subnode = node(entry, source);
if (subnode) { currentQueue.push(subnode); }
let queue = [root];
while (queue.length > 0) {
let entry = queue.shift();
let next = entry.firstChild;

while (next) {
queue.push(next);
next = next.nextSibling;
}

// Check the status of current task to see if we should yield before continuing
let state = task.state(timer);
if (state === Task.Wait) { state = await task.suspend(timer); }
if (state === Task.Stop) { break; }

// Check if processing a node gives us a pointer to one of its sub nodes for traversal
// E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set
// Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe.
let subnode = node(entry, source);
if (subnode) { queue.push(subnode); }
}
}
}
3 changes: 2 additions & 1 deletion packages/clarity-js/types/data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export const enum Metric {
MaxTouchPoints = 32,
HardwareConcurrency = 33,
DeviceMemory = 34,
Electron = 35
Electron = 35,
ConstructedStyles = 36
}

export const enum Dimension {
Expand Down
4 changes: 2 additions & 2 deletions packages/clarity-visualize/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-visualize",
"version": "0.7.25",
"version": "0.7.26",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand All @@ -27,7 +27,7 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-decode": "^0.7.25"
"clarity-decode": "^0.7.26"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.0",
Expand Down
43 changes: 7 additions & 36 deletions packages/clarity-visualize/src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export class LayoutHelper {
animations = {};
state: PlaybackState = null;
stylesToApply: { [id: string] : string[] } = {};
styleSheetMap: { [id: number] : string[]; } = {};

constructor(state: PlaybackState) {
this.state = state;
Expand Down Expand Up @@ -115,18 +114,10 @@ export class LayoutHelper {
}
switch (event.data.operation) {
case StyleSheetOperation.Create:
this.adoptedStyleSheets[event.data.id] = new CSSStyleSheet();
this.adoptedStyleSheets[event.data.id] = new (this.state.window as any).CSSStyleSheet();
break;
case StyleSheetOperation.Replace:
styleSheet.replace(event.data.cssRules);
// Just changing the sheet isn't sufficient as we cannot rely on adoptedStyleSheets in visualiation
// when an underlying style sheet changes, we reset the styles on the element
for (var documentIdAsString of Object.keys(this.styleSheetMap)) {
var documentId = parseInt(documentIdAsString, 10);
if (this.styleSheetMap[documentId].indexOf(event.data.id as string) > -1) {
this.setDocumentStyles(documentId, this.styleSheetMap[documentId]);
}
}
break;
case StyleSheetOperation.ReplaceSync:
styleSheet.replaceSync(event.data.cssRules);
Expand All @@ -150,34 +141,14 @@ export class LayoutHelper {
return;
}

this.styleSheetMap[documentId] = styleIds;
let newSheets = styleIds.map(x => this.adoptedStyleSheets[x] as CSSStyleSheet);

let styleNode = targetDocument.getElementById(Constant.AdoptedStyleSheet) ?? this.state.window.document.createElement("style");
styleNode.id = Constant.AdoptedStyleSheet;
let ruleLengths = [];
styleNode.textContent = newSheets.map(x => { let newRule = this.getCssRules(x); ruleLengths.push(newRule.length); return newRule; }).join('\n');
styleNode.setAttribute('data-parentid', `${documentId}`);
if (targetDocument.head) {
targetDocument.head.appendChild(styleNode);
} else {
targetDocument.appendChild(styleNode);
}
}

private getCssRules(sheet: CSSStyleSheet): string {
let value = Constant.Empty as string;
let cssRules = null;
try { cssRules = sheet ? sheet.cssRules : []; } catch (e) {
if (e && e.name !== "SecurityError") { throw e; }
}

if (cssRules !== null) {
for (let i = 0; i < cssRules.length; i++) {
value += cssRules[i].cssText;
let newSheets: CSSStyleSheet[] = [];
for (var styleId of styleIds) {
let styleSheet = this.adoptedStyleSheets[styleId];
if (styleSheet) {
newSheets.push(styleSheet);
}
}
return value;
targetDocument.adoptedStyleSheets = newSheets
}

public exists = (hash: string): boolean => {
Expand Down

0 comments on commit 235b9b1

Please sign in to comment.