Skip to content

Commit c831911

Browse files
authored
cypress: cleanup nasty waitInDevMode (#380)
aborted requests when component is unmounting which usually happens in cypress tests since we click around faster than the server responds
1 parent ce5fb76 commit c831911

File tree

8 files changed

+113
-24
lines changed

8 files changed

+113
-24
lines changed

cypress/integration/basic.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,13 @@ describe('basic test', () => {
88
it('internal sidebar links work', () => {
99
cy.visit('/')
1010

11-
waitInDevMode(100);
12-
1311
cy.findByTestId('sidebar-comparison').click();
14-
waitInDevMode(100);
1512
cy.location('pathname').should('eq', '/comparison');
1613

1714
cy.findByTestId('sidebar-comparison-diff').click();
18-
waitInDevMode(100);
1915
cy.location('pathname').should('eq', '/comparison-diff');
2016

2117
cy.findByTestId('sidebar-root').click();
22-
waitInDevMode(100);
2318
cy.location('pathname').should('eq', '/');
2419
});
2520
})
26-
27-
// very nasty, just to avoid dealing with the following error
28-
// which requires aborting fetch call and whatnot
29-
// react-dom.development.js:21 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
30-
// in FlameGraphRenderer (created by Context.Consumer)
31-
// in e (created by ConnectFunction)
32-
// in ConnectFunction (created by PyroscopeApp)
33-
// in div (created by PyroscopeApp)
34-
// in div (created by PyroscopeApp)
35-
// in PyroscopeApp (created by ConnectFunction)
36-
// in ConnectFunction
37-
function waitInDevMode(t: number) {
38-
if (!process.env.CI) {
39-
cy.wait(t);
40-
}
41-
}

webapp/javascript/components/ComparisonApp.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ function ComparisonApp(props) {
2020
if (prevPropsRef.renderURL !== renderURL) {
2121
actions.fetchTimeline(renderURL);
2222
}
23+
24+
return actions.abortTimelineRequest;
2325
}, [renderURL]);
2426

2527
return (

webapp/javascript/components/ComparisonDiffApp.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function ComparisonDiffApp(props) {
1616
if (prevPropsRef.diffRenderURL !== diffRenderURL) {
1717
actions.fetchTimeline(diffRenderURL);
1818
}
19+
return actions.abortTimelineRequest;
1920
}, [diffRenderURL]);
2021

2122
return (

webapp/javascript/components/FlameGraphRenderer.jsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import ProfilerHeader from "./ProfilerHeader";
4242
import { deltaDiffWrapper, parseFlamebearerFormat } from "../util/flamebearer";
4343

4444
import ExportData from "./ExportData";
45+
import {isAbortError} from "../util/abort";
4546

4647

4748
const PX_PER_LEVEL = 18;
@@ -143,10 +144,18 @@ class FlameGraphRenderer extends React.Component {
143144
}
144145
}
145146

146-
fetchFlameBearerData(url) {
147+
componentWillUnmount() {
148+
this.abortCurrentJSONController();
149+
}
150+
151+
abortCurrentJSONController() {
147152
if (this.currentJSONController) {
148153
this.currentJSONController.abort();
149154
}
155+
}
156+
157+
fetchFlameBearerData(url) {
158+
this.abortCurrentJSONController();
150159
this.currentJSONController = new AbortController();
151160

152161
fetch(`${url}&format=json`, { signal: this.currentJSONController.signal })
@@ -161,6 +170,12 @@ class FlameGraphRenderer extends React.Component {
161170
this.updateData();
162171
})
163172
})
173+
.catch(e => {
174+
// AbortErrors are fine
175+
if (!isAbortError(e)) {
176+
throw e;
177+
}
178+
})
164179
.finally();
165180
}
166181

webapp/javascript/components/PyroscopeApp.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import TimelineChartWrapper from "./TimelineChartWrapper";
88
import Header from "./Header";
99
import Footer from "./Footer";
1010
import { buildRenderURL } from "../util/updateRequests";
11-
import { fetchNames, fetchTimeline } from "../redux/actions";
11+
import { fetchNames, fetchTimeline, abortTimelineRequest } from "../redux/actions";
1212

1313
function PyroscopeApp(props) {
1414
const { actions, renderURL } = props;
@@ -18,6 +18,8 @@ function PyroscopeApp(props) {
1818
if (prevPropsRef.renderURL !== renderURL) {
1919
actions.fetchTimeline(renderURL);
2020
}
21+
22+
return actions.abortTimelineRequest;
2123
}, [renderURL]);
2224

2325
return (
@@ -42,6 +44,7 @@ const mapDispatchToProps = (dispatch) => ({
4244
{
4345
fetchTimeline,
4446
fetchNames,
47+
abortTimelineRequest,
4548
},
4649
dispatch
4750
),

webapp/javascript/components/TagsBar.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { connect } from "react-redux";
44
import "react-dom";
55
import { Menu, SubMenu, MenuItem, MenuButton } from "@szhsin/react-menu";
66

7-
import { fetchTags, fetchTagValues, setQuery } from "../redux/actions";
7+
import { fetchTags, fetchTagValues, setQuery, abortFetchTags, abortFetchTagValues } from "../redux/actions";
88
import "../util/prism";
99

1010
function TagsBar({ query, actions, tags, tagValuesLoading }) {
@@ -16,6 +16,8 @@ function TagsBar({ query, actions, tags, tagValuesLoading }) {
1616

1717
useEffect(() => {
1818
actions.fetchTags(query);
19+
20+
return actions.abortFetchTags;
1921
}, [query]);
2022

2123
const submitTagsValue = (newValue) => {
@@ -40,6 +42,10 @@ function TagsBar({ query, actions, tags, tagValuesLoading }) {
4042
const loadTagValues = (tag) => {
4143
actions.fetchTagValues(query, tag);
4244
};
45+
useEffect(() => {
46+
// since fetchTagValues may be running
47+
return actions.abortFetchTagValues;
48+
}, [])
4349

4450
const onTagsValueChange = (tagKey, tagValue) => {
4551
let newQuery;
@@ -135,6 +141,7 @@ const mapDispatchToProps = (dispatch) => ({
135141
{
136142
fetchTags,
137143
fetchTagValues,
144+
abortFetchTags,
138145
setQuery,
139146
},
140147
dispatch

webapp/javascript/redux/actions.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SET_RIGHT_FROM,
2121
SET_RIGHT_UNTIL,
2222
} from "./actionTypes";
23+
import { isAbortError } from "../util/abort";
2324

2425
export const setDateRange = (from, until) => ({
2526
type: SET_DATE_RANGE,
@@ -103,7 +104,15 @@ export const setQuery = (query) => ({
103104
payload: { query },
104105
});
105106

107+
/**
108+
* ATTENTION! There may be race conditions:
109+
* Since a new controller is created every time a 'fetch' action is called
110+
* A badly timed 'abort' action may cancel the brand new 'fetch' action!
111+
*/
106112
let currentTimelineController;
113+
let fetchTagController;
114+
let fetchTagValuesController;
115+
107116
export function fetchTimeline(url) {
108117
return (dispatch) => {
109118
if (currentTimelineController) {
@@ -118,24 +127,62 @@ export function fetchTimeline(url) {
118127
.then((data) => {
119128
dispatch(receiveTimeline(data));
120129
})
130+
.catch((e) => {
131+
// AbortErrors are fine
132+
if (!isAbortError(e)) {
133+
throw e;
134+
}
135+
})
121136
.finally();
122137
};
123138
}
124139

140+
export function abortTimelineRequest() {
141+
return () => {
142+
if (currentTimelineController) {
143+
currentTimelineController.abort();
144+
}
145+
};
146+
}
147+
125148
export function fetchTags(query) {
126149
return (dispatch) => {
150+
if (fetchTagController) {
151+
fetchTagController.abort();
152+
}
153+
fetchTagController = new AbortController();
154+
127155
dispatch(requestTags());
128156
return fetch(`/labels?query=${encodeURIComponent(query)}`)
129157
.then((response) => response.json())
130158
.then((data) => {
131159
dispatch(receiveTags(data));
132160
})
161+
.catch((e) => {
162+
// AbortErrors are fine
163+
if (!isAbortError(e)) {
164+
throw e;
165+
}
166+
})
133167
.finally();
134168
};
135169
}
136170

171+
export function abortFetchTags() {
172+
return () => {
173+
if (fetchTagController) {
174+
fetchTagController.abort();
175+
}
176+
};
177+
}
178+
137179
export function fetchTagValues(query, tag) {
138180
return (dispatch) => {
181+
if (fetchTagValuesController) {
182+
fetchTagValuesController.abort();
183+
}
184+
fetchTagValuesController = new AbortController();
185+
139186
dispatch(requestTagValues(tag));
140187
return fetch(
141188
`/label-values?label=${encodeURIComponent(
@@ -146,9 +193,22 @@ export function fetchTagValues(query, tag) {
146193
.then((data) => {
147194
dispatch(receiveTagValues(data, tag));
148195
})
196+
.catch((e) => {
197+
// AbortErrors are fine
198+
if (!fetchTagValuesController.signal.aborted) {
199+
throw e;
200+
}
201+
})
149202
.finally();
150203
};
151204
}
205+
export function abortFetchTagValues() {
206+
return () => {
207+
if (fetchTagValuesController) {
208+
fetchTagValuesController.abort();
209+
}
210+
};
211+
}
152212

153213
let currentNamesController;
154214
export function fetchNames() {
@@ -166,6 +226,19 @@ export function fetchNames() {
166226
.then((data) => {
167227
dispatch(receiveNames(data));
168228
})
229+
.catch((e) => {
230+
// AbortErrors are fine
231+
if (!isAbortError(e)) {
232+
throw e;
233+
}
234+
})
169235
.finally();
170236
};
171237
}
238+
export function abortFetchNames() {
239+
return () => {
240+
if (abortFetchNames) {
241+
abortFetchNames.abort();
242+
}
243+
};
244+
}

webapp/javascript/util/abort.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function isAbortError(err) {
2+
if (!err) {
3+
return false;
4+
}
5+
6+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
7+
return err.name === 'AbortError'
8+
|| error.code === 20;
9+
}

0 commit comments

Comments
 (0)