Skip to content

Commit

Permalink
Merge pull request #5200 from inception-project/refactoring/5159-Impr…
Browse files Browse the repository at this point in the history
…ove-HTML-render-performance

Issue #5159: Improve HTML render performance
  • Loading branch information
reckart authored Dec 10, 2024
2 parents 71ca382 + ffb8c0d commit b498610
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ html > body {
* Annotation highlight.
*/
.iaa-highlighted {
transition: filter var(--anim-fast);
/* transition: filter var(--anim-fast); -- slow for large documents, so we disable it here */
cursor: pointer;
background-color: var(--iaa-background-color);
box-sizing: border-box;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ export class ApacheAnnotatorVisualizer {
private tracker: ViewportTracker
private showInlineLabels = false
private showEmptyHighlights = false
private observer: IntersectionObserver

private sectionSelector: string
private sectionAnnotationVisualizer: SectionAnnotationVisualizer
private sectionAnnotationCreator: SectionAnnotationCreator
private showAggregatedLabels = true

private data? : AnnotatedText

private removeTransientMarkers: (() => void)[] = []
private removeTransientMarkersTimeout: number | undefined = undefined
private scrolling = false
private lastScrollTop: number | undefined = undefined
private removeScrollMarkers: (() => void)[] = []
private removeScrollMarkersTimeout: number | undefined = undefined
private removePingMarkers: (() => void)[] = []
private removePingMarkersTimeout: number | undefined = undefined

private alpha = '55'

Expand Down Expand Up @@ -129,6 +133,11 @@ export class ApacheAnnotatorVisualizer {
}

loadAnnotations (): void {
// scrollTo uses a timeout to work around the problem that the browser does not always properly
// scroll to the target element. We want to avoid loading annotations while scrolling is still
// in progress. Once scrolling is complete, we should get triggered by the ViewportTracker.
if (this.scrolling) return

const options: DiamLoadAnnotationsOptions = {
range: this.tracker.currentRange,
includeText: false,
Expand All @@ -146,7 +155,8 @@ export class ApacheAnnotatorVisualizer {
}

private renderAnnotations (doc: AnnotatedText): void {
const startTime = new Date().getTime()
console.log(`Client-side starting`)
const startTime = performance.now()

this.clearHighlights()
this.resizer.hide()
Expand Down Expand Up @@ -181,8 +191,8 @@ export class ApacheAnnotatorVisualizer {
this.renderSelectedRelationEndpointHighlights(doc)
}

const endTime = new Date().getTime()
console.log(`Client-side rendering took ${Math.abs(endTime - startTime)}ms`)
const endTime = performance.now()
console.log(`Client-side rendering took ${endTime - startTime}ms`)
}

private renderVerticalSelectionMarker (doc: AnnotatedText) {
Expand Down Expand Up @@ -262,17 +272,17 @@ export class ApacheAnnotatorVisualizer {
* Some highlights may only contain whitepace. This method removes such highlights.
*/
private removeWhitepaceOnlyHighlights (selector: string = '.iaa-highlighted') {
let candidates = this.root.querySelectorAll(selector)
const start = performance.now();
const candidates = this.root.querySelectorAll(selector)
console.log(`Found ${candidates.length} elements matching [${selector}] to remove whitespace-only highlights`)
let start = performance.now();
candidates.forEach(e => {
if (!e.classList.contains('iaa-zero-width') && !e.textContent?.trim()) {
e.after(...e.childNodes)
e.remove()
}
})
let end = performance.now();
console.log(`Time taken: ${end - start} milliseconds`)
const end = performance.now();
console.log(`Removing whitespace only highlights took ${end - start}ms`)
}

private postProcessHighlights () {
Expand Down Expand Up @@ -373,14 +383,14 @@ export class ApacheAnnotatorVisualizer {

if (viewportBegin <= begin && end <= viewportEnd) {
// Quick and easy if the annotation fits entirely into the visible viewport
const startTime = new Date().getTime()
const startTime = performance.now()
this.renderHighlight(span, begin, end, attributes)
const endTime = new Date().getTime()
const endTime = performance.now()
// console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms`)
} else {
// Try optimizing for long spans to improve rendering performance
let fragmentCount = 0
const startTime = new Date().getTime()
const startTime = performance.now()

const coreBegin = Math.max(begin, viewportBegin)
const coreEnd = Math.min(end, viewportEnd)
Expand All @@ -399,7 +409,7 @@ export class ApacheAnnotatorVisualizer {
this.renderHighlight(span, end, end, attributes)
fragmentCount++
}
const endTime = new Date().getTime()
const endTime = performance.now()
// console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms (${fragmentCount} fragments)`)
}
}
Expand All @@ -419,21 +429,56 @@ export class ApacheAnnotatorVisualizer {
this.toCleanUp.add(highlightText(range, 'mark', attributes))
}

private clearScrollMarkers () {
if (this.removeScrollMarkersTimeout) {
window.cancelIdleCallback(this.removeScrollMarkersTimeout)
this.removeScrollMarkersTimeout = undefined
this.removeScrollMarkers.forEach(remove => remove())
this.removeScrollMarkers = []
}
}

private renderPingMarkers(pingRanges?: Offsets[]) {
if (!pingRanges) return

console.log('Rendering ping markers')

for (const pingOffset of pingRanges || []) {
const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1])
if (pingRange) {
this.removePingMarkers.push(this.safeHighlightText(pingRange, 'mark', { class: 'iaa-ping-marker' }))
}
}

this.removeWhitepaceOnlyHighlights('.iaa-ping-marker')
this.removeSpuriousZeroWidthHighlights()

if (this.removePingMarkers.length > 0) {
this.removePingMarkersTimeout = window.setTimeout(() => this.clearPingMarkers(), 2000)
}
}

private clearPingMarkers () {
console.log('Clearing ping markers');

if (this.removePingMarkersTimeout) {
window.clearTimeout(this.removePingMarkersTimeout)
this.removePingMarkersTimeout = undefined
this.removePingMarkers.forEach(remove => remove())
this.removePingMarkers = []
}
}

scrollTo (args: { offset: number, position?: string, pingRanges?: Offsets[] }): void {
const range = offsetToRange(this.root, args.offset, args.offset)
if (!range) return

window.clearTimeout(this.removeTransientMarkersTimeout)
this.removeTransientMarkers.forEach(remove => remove())
this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
this.clearScrollMarkers()
this.clearPingMarkers()

// Add scroll marker
const removeScrollMarker = highlightText(range, 'mark', { id: 'iaa-scroll-marker' })
this.removeTransientMarkers = [removeScrollMarker]
for (const pingOffset of args.pingRanges || []) {
const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1])
if (!pingRange) continue
this.removeTransientMarkers.push(highlightText(pingRange, 'mark', { class: 'iaa-ping-marker' }))
}
this.removeScrollMarkers = [removeScrollMarker]

if (!this.showEmptyHighlights) {
this.removeWhitepaceOnlyHighlights('.iaa-ping-marker')
Expand Down Expand Up @@ -466,17 +511,38 @@ export class ApacheAnnotatorVisualizer {
// markers are still there.
var scrollIntoViewFunc = () => {
finalScrollTarget.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' })
if (this.removeTransientMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100)
if (this.removePingMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100)

if (this.root instanceof HTMLElement) {
if (this.root.scrollTop === this.lastScrollTop) {
this.scrollToComplete(args.pingRanges)
}
else {
this.lastScrollTop = this.root.scrollTop
}
}
}

this.scrolling = true
this.sectionAnnotationVisualizer.suspend()
this.sectionAnnotationCreator.suspend()
window.setTimeout(scrollIntoViewFunc, 100)
}

this.removeTransientMarkersTimeout = window.setTimeout(() => {
this.removeTransientMarkers.forEach(remove => remove())
this.removeTransientMarkers = []
this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
}, 2000)
this.removeScrollMarkersTimeout = window.requestIdleCallback(() => this.scrollToComplete(args.pingRanges), { timeout: 2000 })
}

private scrollToComplete(pingRanges?: Offsets[]) {
console.log('Scrolling complete')

this.clearScrollMarkers()
// this.renderPingMarkers(pingRanges)
this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120

this.scrolling = false
this.sectionAnnotationCreator.resume()
this.sectionAnnotationVisualizer.resume()
this.lastScrollTop = undefined
}

private clearHighlights (): void {
Expand All @@ -486,20 +552,19 @@ export class ApacheAnnotatorVisualizer {
return
}

const startTime = new Date().getTime()
const startTime = performance.now()
const highlightCount = this.toCleanUp.size
this.toCleanUp.forEach(cleanup => cleanup())
this.toCleanUp.clear()
this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
const endTime = new Date().getTime()
const endTime = performance.now()
console.log(`Cleaning up ${highlightCount} annotations and normalizing DOM took ${Math.abs(endTime - startTime)}ms`)
}

destroy (): void {
if (this.observer) {
this.observer.disconnect()
}

this.sectionAnnotationCreator.destroy()
this.sectionAnnotationVisualizer.destroy()
this.tracker.disconnect()
this.clearHighlights()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
*/
import './SectionAnnotationCreator.scss'
import { AnnotatedText, calculateEndOffset, calculateStartOffset, DiamAjax } from "@inception-project/inception-js-api"
import { getScrollY } from './SectionAnnotationVisualizer'

export class SectionAnnotationCreator {
private sectionSelector: string
private ajax: DiamAjax
private root: Element

private observer: IntersectionObserver
private observerDebounceTimeout: number | undefined
private suspended = false

private _previewFrame: HTMLIFrameElement | undefined
private previewRenderTimeout: number | undefined
private previewScrollTimeout: number | undefined
Expand All @@ -38,6 +43,20 @@ export class SectionAnnotationCreator {
}
}

public suspend() {
this.suspended = true
}

public resume() {
this.suspended = false
}

public destroy() {
this.observer.disconnect()
this.root.querySelectorAll('.iaa-section-control').forEach(e => e.remove())
this.hidePreviewFrame()
}

private initializeSectionTypeAttributes() {
this.root.querySelectorAll(this.sectionSelector).forEach((e, i) => {
e.setAttribute('data-iaa-section-type', e.localName)
Expand All @@ -62,9 +81,10 @@ export class SectionAnnotationCreator {
}

private ensureVisibility() {
const rootRect = this.root.getBoundingClientRect()
const scrollY = (this.root.scrollTop || 0) - rootRect.top
const scrollY = getScrollY(this.root)
const panels = Array.from(this.root.querySelectorAll('.iaa-section-control') || [])

const panelsTops = new Map<HTMLElement, number>()
for (const panel of (panels as HTMLElement[])) {
const sectionId = panel.getAttribute('data-iaa-applies-to')
const section = this.root.querySelector(`[id="${sectionId}"]`)
Expand All @@ -73,30 +93,42 @@ export class SectionAnnotationCreator {
continue
}
const sectionRect = section.getBoundingClientRect()
panel.style.top = `${sectionRect.top + scrollY}px`
panelsTops.set(panel, sectionRect.top)
}

// Update the position of the panels all at once to avoid layout thrashing
for (const [panel, top] of panelsTops) {
panel.style.top = `${top + scrollY}px`
}
}

private handleIntersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
const rootRect = this.root.getBoundingClientRect()
const scrollY = (this.root.scrollTop || 0) - rootRect.top

for (const entry of entries) {
const sectionId = entry.target.id
const sectionRect = entry.boundingClientRect
let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement

if (entry.isIntersecting && !panel) {
panel = this.createControl()
panel.setAttribute('data-iaa-applies-to', sectionId)
panel.style.top = `${sectionRect.top + scrollY}px`
this.root.appendChild(panel)
}
if (this.observerDebounceTimeout) {
window.cancelIdleCallback(this.observerDebounceTimeout)
this.observerDebounceTimeout = undefined
}

if (!entry.isIntersecting && panel) {
panel.remove()
this.observerDebounceTimeout = window.requestIdleCallback(() => {
const rootRect = this.root.getBoundingClientRect()
const scrollY = (this.root.scrollTop || 0) - rootRect.top

for (const entry of entries) {
const sectionId = entry.target.id
const sectionRect = entry.boundingClientRect
let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement

if (entry.isIntersecting && !panel) {
panel = this.createControl()
panel.setAttribute('data-iaa-applies-to', sectionId)
panel.style.top = `${sectionRect.top + scrollY}px`
this.root.appendChild(panel)
}

if (!entry.isIntersecting && panel) {
panel.remove()
}
}
}
}, { timeout: 100 })
}

private createControl(): HTMLElement {
Expand Down
Loading

0 comments on commit b498610

Please sign in to comment.