Skip to content

Commit f7c7899

Browse files
authored
Fix loading performance for pages with many links caused by excessive re-computation (#900)
Resolves: rdar://136247329
1 parent bc2d4bc commit f7c7899

10 files changed

+167
-86
lines changed

src/components/DocumentationTopic.vue

+6
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ export default {
227227
return {
228228
reset() {},
229229
state: {},
230+
setReferences() {},
231+
updateReferences() {},
230232
};
231233
},
232234
},
@@ -423,6 +425,7 @@ export default {
423425
},
424426
data() {
425427
return {
428+
appState: AppStore.state,
426429
topicState: this.store.state,
427430
declListExpanded: false, // Hide all other declarations by default
428431
};
@@ -718,6 +721,9 @@ export default {
718721
this.store.setReferences(this.references);
719722
},
720723
watch: {
724+
'appState.includedArchiveIdentifiers': function updateRefs() {
725+
this.store.updateReferences();
726+
},
721727
// update the references in the store, in case they update, but the component is not re-created
722728
references(references) {
723729
this.store.setReferences(references);

src/mixins/referencesProvider.js

+1-43
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77
* See https://swift.org/LICENSE.txt for license information
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
10-
import AppStore from 'docc-render/stores/AppStore';
11-
12-
const TopicReferenceTypes = new Set([
13-
'section',
14-
'topic',
15-
]);
1610

1711
export default {
1812
// inject the `store`
@@ -27,43 +21,7 @@ export default {
2721
}),
2822
},
2923
},
30-
data: () => ({ appState: AppStore.state }),
3124
computed: {
32-
// exposes the references for the current page
33-
references() {
34-
const {
35-
isFromIncludedArchive,
36-
store: {
37-
state: { references: originalRefs = {} },
38-
},
39-
} = this;
40-
// strip the `url` key from "topic"/"section" refs if their identifier
41-
// comes from an archive that hasn't been included by DocC
42-
return Object.keys(originalRefs).reduce((newRefs, id) => {
43-
const originalRef = originalRefs[id];
44-
const { url, ...refWithoutUrl } = originalRef;
45-
return TopicReferenceTypes.has(originalRef.type) ? ({
46-
...newRefs,
47-
[id]: isFromIncludedArchive(id) ? originalRefs[id] : refWithoutUrl,
48-
}) : ({
49-
...newRefs,
50-
[id]: originalRef,
51-
});
52-
}, {});
53-
},
54-
},
55-
methods: {
56-
isFromIncludedArchive(id) {
57-
const { includedArchiveIdentifiers = [] } = this.appState;
58-
// for backwards compatibility purposes, treat all references as being
59-
// from included archives if there is no data for it
60-
if (!includedArchiveIdentifiers.length) {
61-
return true;
62-
}
63-
64-
return includedArchiveIdentifiers.some(archiveId => (
65-
id?.startsWith(`doc://${archiveId}/`)
66-
));
67-
},
25+
references: ({ store }) => store.state.references,
6826
},
6927
};

src/stores/DocumentationTopicStore.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
import { filterInactiveReferences } from 'docc-render/utils/references';
1112
import ApiChangesStoreBase from 'docc-render/stores/ApiChangesStoreBase';
1213
import OnThisPageSectionsStoreBase from 'docc-render/stores/OnThisPageSectionsStoreBase';
1314
import Settings from 'docc-render/utils/settings';
@@ -36,7 +37,10 @@ export default {
3637
this.state.contentWidth = width;
3738
},
3839
setReferences(references) {
39-
this.state.references = references;
40+
this.state.references = filterInactiveReferences(references);
41+
},
42+
updateReferences() {
43+
this.setReferences(this.state.references);
4044
},
4145
...changesActions,
4246
...pageSectionsActions,

src/utils/references.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import AppStore from 'docc-render/stores/AppStore';
12+
13+
const TopicReferenceTypes = new Set([
14+
'section',
15+
'topic',
16+
]);
17+
18+
function isFromIncludedArchive(id) {
19+
const { includedArchiveIdentifiers } = AppStore.state;
20+
21+
// for backwards compatibility purposes, treat all references as being
22+
// from included archives if there is no data for it
23+
if (!includedArchiveIdentifiers.length) {
24+
return true;
25+
}
26+
27+
return includedArchiveIdentifiers.some(archiveId => (
28+
id?.startsWith(`doc://${archiveId}/`)
29+
));
30+
}
31+
32+
function filterInactiveReferences(references = {}) {
33+
return Object.keys(references).reduce((newRefs, id) => {
34+
const originalRef = references[id];
35+
const { url, ...refWithoutUrl } = originalRef;
36+
return TopicReferenceTypes.has(originalRef.type) ? ({
37+
...newRefs,
38+
[id]: isFromIncludedArchive(id) ? originalRef : refWithoutUrl,
39+
}) : ({
40+
...newRefs,
41+
[id]: originalRef,
42+
});
43+
}, {});
44+
}
45+
46+
// eslint-disable-next-line import/prefer-default-export
47+
export { filterInactiveReferences };

tests/unit/components/DocumentationTopic.spec.js

+20
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,26 @@ describe('DocumentationTopic', () => {
11711171
expect(mockStore.setReferences).toHaveBeenCalledWith(newReferences);
11721172
});
11731173

1174+
it('calls `store.updateReferences` when `appState.includedArchiveIdentifiers` changes', async () => {
1175+
const store = {
1176+
state: { references: {} },
1177+
reset: jest.fn(),
1178+
setReferences: jest.fn(),
1179+
updateReferences: jest.fn(),
1180+
};
1181+
wrapper = shallowMount(DocumentationTopic, {
1182+
propsData,
1183+
provide: { store },
1184+
});
1185+
expect(store.updateReferences).not.toHaveBeenCalled();
1186+
1187+
wrapper.setData({
1188+
appState: { includedArchiveIdentifiers: ['Foo', 'Bar'] },
1189+
});
1190+
await wrapper.vm.$nextTick();
1191+
expect(store.updateReferences).toHaveBeenCalled();
1192+
});
1193+
11741194
describe('lifecycle hooks', () => {
11751195
it('calls `store.reset()`', () => {
11761196
jest.clearAllMocks();

tests/unit/mixins/referencesProvider.spec.js

-39
Original file line numberDiff line numberDiff line change
@@ -91,43 +91,4 @@ describe('referencesProvider', () => {
9191
expect(inner.exists()).toBe(true);
9292
expect(inner.props('references')).toEqual(references);
9393
});
94-
95-
it('removes `url` data for refs with non-empty `includedArchiveIdentifiers` app state', () => {
96-
// empty `includedArchiveIdentifiers` — no changes to refs
97-
const outer = createOuter();
98-
let inner = outer.find(FakeComponentInner);
99-
expect(inner.exists()).toBe(true);
100-
expect(inner.props('references')).toEqual(references);
101-
102-
// `includedArchiveIdentifiers` contains all refs - no changes to refs
103-
outer.setData({
104-
appState: {
105-
includedArchiveIdentifiers: ['A', 'B', 'BB'],
106-
},
107-
});
108-
inner = outer.find(FakeComponentInner);
109-
expect(inner.exists()).toBe(true);
110-
expect(inner.props('references')).toEqual(references);
111-
112-
// `includedArchiveIdentifiers` only contains archive B — remove `url` field
113-
// from all non-B refs
114-
outer.setData({
115-
appState: {
116-
includedArchiveIdentifiers: ['B'],
117-
},
118-
});
119-
inner = outer.find(FakeComponentInner);
120-
expect(inner.exists()).toBe(true);
121-
const refs3 = inner.props('references');
122-
expect(refs3).not.toEqual(references);
123-
expect(refs3[aa.identifier].title).toBe(aa.title);
124-
expect(refs3[aa.identifier].url).toBeFalsy(); // aa `url` is gone now
125-
expect(refs3[ab.identifier].title).toBe(ab.title);
126-
expect(refs3[ab.identifier].url).toBeFalsy(); // ab `url` is gone now
127-
expect(refs3[bb.identifier].title).toBe(bb.title);
128-
expect(refs3[bb.identifier].url).toBe(bb.url); // bb still has `url`
129-
expect(refs3[bbb.identifier].title).toBe(bbb.title);
130-
expect(refs3[bbb.identifier].url).toBeFalsy(); // bbb `url` is gone now
131-
expect(refs3[c.identifier].url).toBe(c.url); // external link untouched
132-
});
13394
});

tests/unit/stores/DocumentationTopicStore.spec.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
import { filterInactiveReferences } from 'docc-render/utils/references';
1112
import DocumentationTopicStore from 'docc-render/stores/DocumentationTopicStore';
1213
import ApiChangesStoreBase from 'docc-render/stores/ApiChangesStoreBase';
1314
import OnThisPageSectionsStoreBase from 'docc-render/stores/OnThisPageSectionsStoreBase';
@@ -101,7 +102,15 @@ describe('DocumentationTopicStore', () => {
101102

102103
it('sets `references`', () => {
103104
DocumentationTopicStore.setReferences(references);
104-
expect(DocumentationTopicStore.state.references).toBe(references);
105+
expect(DocumentationTopicStore.state.references)
106+
.toEqual(filterInactiveReferences(references));
107+
});
108+
109+
it('updates `references`', () => {
110+
const prevState = DocumentationTopicStore.state;
111+
DocumentationTopicStore.updateReferences();
112+
expect(DocumentationTopicStore.state.references)
113+
.toEqual(filterInactiveReferences(prevState.references));
105114
});
106115

107116
describe('APIChanges', () => {

tests/unit/stores/TopicStore.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe('TopicStore', () => {
123123
describe('setReferences', () => {
124124
it('sets the `references` state', () => {
125125
TopicStore.setReferences(references);
126-
expect(TopicStore.state.references).toBe(references);
126+
expect(TopicStore.state.references).toEqual(references);
127127
});
128128
});
129129
});

tests/unit/stores/TutorialsOverviewStore.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('TutorialsOverviewStore', () => {
5555
describe('setReferences', () => {
5656
it('sets the `references` state', () => {
5757
TutorialsOverviewStore.setReferences(references);
58-
expect(TutorialsOverviewStore.state.references).toBe(references);
58+
expect(TutorialsOverviewStore.state.references).toEqual(references);
5959
});
6060
});
6161
});

tests/unit/utils/references.spec.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import AppStore from 'docc-render/stores/AppStore';
12+
import { filterInactiveReferences } from 'docc-render/utils/references';
13+
14+
const aa = {
15+
identifier: 'doc://A/documentation/A/a',
16+
url: '/documentation/A/a',
17+
title: 'A.A',
18+
type: 'topic',
19+
};
20+
const ab = {
21+
identifier: 'doc://A/documentation/A/b',
22+
url: '/documentation/A/b',
23+
title: 'A.B',
24+
type: 'topic',
25+
};
26+
const bb = {
27+
identifier: 'doc://B/documentation/B/b',
28+
url: '/documentation/B/b',
29+
title: 'B.B',
30+
type: 'topic',
31+
};
32+
const bbb = {
33+
identifier: 'doc://BB/documentation/BB/b',
34+
url: '/documentation/BB/b#b',
35+
title: 'BB.B',
36+
type: 'section',
37+
};
38+
const c = {
39+
identifier: 'https://abc.dev',
40+
url: 'https://abc.dev',
41+
title: 'C',
42+
type: 'link',
43+
};
44+
45+
const references = {
46+
[aa.identifier]: aa,
47+
[ab.identifier]: ab,
48+
[bb.identifier]: bb,
49+
[bbb.identifier]: bbb,
50+
[c.identifier]: c,
51+
};
52+
53+
describe('filterInactiveReferences', () => {
54+
it('does not filter any refs when `includedArchiveIdentifiers` is empty', () => {
55+
AppStore.setIncludedArchiveIdentifiers([]);
56+
expect(filterInactiveReferences(references)).toEqual(references);
57+
});
58+
59+
it('does not filter any refs when `includedArchiveIdentifiers` includes all ref archives', () => {
60+
AppStore.setIncludedArchiveIdentifiers(['A', 'B', 'BB']);
61+
expect(filterInactiveReferences(references)).toEqual(references);
62+
});
63+
64+
it('removes `url` from non-external refs that aren\'t part of included archive', () => {
65+
AppStore.setIncludedArchiveIdentifiers(['B']);
66+
const filteredRefs = filterInactiveReferences(references);
67+
68+
expect(Object.keys(filteredRefs)).toEqual(Object.keys(references));
69+
70+
expect(filteredRefs[aa.identifier].url).toBeFalsy();
71+
expect(filteredRefs[ab.identifier].url).toBeFalsy();
72+
expect(filteredRefs[bb.identifier].url).toBe(bb.url);
73+
expect(filteredRefs[bbb.identifier].url).toBeFalsy();
74+
expect(filteredRefs[c.identifier].url).toBe(c.url);
75+
});
76+
});

0 commit comments

Comments
 (0)