Skip to content

Commit 3966966

Browse files
authored
scalar: fix the timing issues (#2825)
Repro case ---------- 1. load scalars 2. simulate slower network connection (on your favorite browser or proxy) 3. toggle on a run 4. toggle on another run 5. (depending on the dataset) you should see nothing and line chart defaults to 0-1 scale. 6. if you double click on the line-chart, the line is shown! Explanation ----------- Setting: `dataToLoad` was [a, b] and "changes" to [a, b]. Before: 1. requests for [a, b] goes out 2. before the requests resolve, second `loadDataImpl` gets invoked and cancels Promises for (1). 3. requests from (1) resolve but because Promise was cancelled, it does not invoke the `onLoadFinish` and domain is not reset After: We now invoke `onLoadFinish` if we ever invoke `loadDataCallback` even once. 1. requests for [a, b] goes out 2. before the requests resolve, second `loadDataImpl` gets invoked but it fetches no data and Promise.all resolves on next tick. Because no data is fetched, it does not call the `onLoadFinish`. 3. requests from (1) resolve and it invokes `onLoadFinish`. Remaining "issue" ----------------- Setting: `dataToLoad` was [a, b] and "changes" to [a, c]. Request to fetch `c` ends before ones for `a` and `b`. Problem: we set the domain based on `c` only but the fix will cause major regression with regarding auto update where any data fetches will cause the chart to rescale even if user overrode the domains.
1 parent b4cde64 commit 3966966

File tree

1 file changed

+77
-43
lines changed

1 file changed

+77
-43
lines changed

tensorboard/components/tf_dashboard_common/data-loader-behavior.ts

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
1515
namespace tf_dashboard_common {
16+
type CacheKey = string;
17+
18+
// NOT_LOADED is implicit
19+
enum LoadState {
20+
LOADING,
21+
LOADED,
22+
}
23+
1624
/**
1725
* @polymerBehavior
1826
*/
@@ -46,7 +54,7 @@ namespace tf_dashboard_common {
4654
*/
4755
getDataLoadName: {
4856
type: Function,
49-
value: () => (datum) => String(datum),
57+
value: () => (datum): CacheKey => String(datum),
5058
},
5159

5260
/**
@@ -90,13 +98,13 @@ namespace tf_dashboard_common {
9098
},
9199

92100
/*
93-
* A set of data that is inflight or has been loaded already. This exists
94-
* to prevent fetching same data again.
101+
* A map of a cache key to LoadState. If a cacheKey does not exist in the
102+
* map, it is considered NOT_LOADED.
95103
* Invoking `reload` or a change in `loadKey` clears the cache.
96104
*/
97-
_inflightOrLoadedData: {
105+
_dataLoadState: {
98106
type: Object,
99-
value: () => new Set(),
107+
value: () => new Map<CacheKey, LoadState>(),
100108
},
101109

102110
_canceller: {
@@ -112,15 +120,16 @@ namespace tf_dashboard_common {
112120
},
113121

114122
reload() {
115-
this._inflightOrLoadedData.clear();
123+
this._dataLoadState.clear();
116124
this._loadData();
117125
},
118126

119127
reset() {
120128
// https://github.com/tensorflow/tensorboard/issues/1499
121129
// Cannot use the observer to observe `loadKey` changes directly.
130+
this.cancelAsync(this._loadDataAsync);
122131
if (this._canceller) this._canceller.cancelAll();
123-
if (this._inflightOrLoadedData) this._inflightOrLoadedData.clear();
132+
if (this._dataLoadState) this._dataLoadState.clear();
124133
if (this.isAttached) this._loadData();
125134
},
126135

@@ -136,7 +145,13 @@ namespace tf_dashboard_common {
136145
},
137146

138147
detached() {
139-
this._canceller.cancelAll();
148+
// Note: Cannot call canceller.cancelAll since it will poison the cache.
149+
// detached gets called when a component gets unmounted from the document
150+
// but it can be re-mounted. When remounted, poisoned cache will manifest.
151+
// t=0: dataLoadState: 'a' = loading
152+
// t=10: unmount
153+
// t=20: request for 'a' resolves but we do not change the loadState
154+
// because we do not want to set one if, instead, it was resetted at t=10.
140155
this.cancelAsync(this._loadDataAsync);
141156
},
142157

@@ -149,43 +164,62 @@ namespace tf_dashboard_common {
149164
_loadDataImpl() {
150165
if (!this.active) return;
151166
this.cancelAsync(this._loadDataAsync);
152-
this._loadDataAsync = this.async(() => {
153-
// Read-only property have a special setter.
154-
this._setDataLoading(true);
155-
156-
// Before updating, cancel any network-pending updates, to
157-
// prevent race conditions where older data stomps newer data.
158-
this._canceller.cancelAll();
159-
const promises = this.dataToLoad
160-
.filter((datum) => {
161-
const name = this.getDataLoadName(datum);
162-
return !this._inflightOrLoadedData.has(name);
163-
})
164-
.map((datum) => {
165-
const cacheKey = this.getDataLoadName(datum);
166-
this._inflightOrLoadedData.add(cacheKey);
167-
return this.requestData(datum).then((value) => {
168-
// dataToLoad can change while the request in-flight and it may
169-
// not be no longer required (loadDataCallback can be an expensive
170-
// proess so we would like to omit it if possible).
171-
const isDataRequiredNow = this.dataToLoad.find(
172-
(datum) => this.getDataLoadName(datum) === cacheKey
167+
this._loadDataAsync = this.async(
168+
this._canceller.cancellable((result) => {
169+
if (result.cancelled) {
170+
return;
171+
}
172+
// Read-only property have a special setter.
173+
this._setDataLoading(true);
174+
175+
// Promises return cacheKeys of the data that were fetched.
176+
const promises = this.dataToLoad
177+
.filter((datum) => {
178+
const cacheKey = this.getDataLoadName(datum);
179+
return !this._dataLoadState.has(cacheKey);
180+
})
181+
.map((datum) => {
182+
const cacheKey = this.getDataLoadName(datum);
183+
this._dataLoadState.set(cacheKey, LoadState.LOADING);
184+
return this.requestData(datum).then(
185+
this._canceller.cancellable((result) => {
186+
// It was resetted. Do not notify of the response.
187+
if (!result.cancelled) {
188+
this._dataLoadState.set(cacheKey, LoadState.LOADED);
189+
this.loadDataCallback(this, datum, result.value);
190+
}
191+
return cacheKey;
192+
})
173193
);
174-
if (isDataRequiredNow) {
175-
this.loadDataCallback(this, datum, value);
176-
}
177194
});
178-
});
179-
180-
return Promise.all(promises).then(
181-
this._canceller.cancellable((result) => {
182-
// Read-only property have a special setter.
183-
this._setDataLoading(false);
184-
if (result.cancelled) return;
185-
this.onLoadFinish();
186-
})
187-
);
188-
});
195+
196+
return Promise.all(promises).then(
197+
this._canceller.cancellable((result) => {
198+
// It was resetted. Do not notify of the data load.
199+
if (!result.cancelled) {
200+
const keysFetched = result.value;
201+
const fetched = new Set(keysFetched);
202+
const shouldNotify = this.dataToLoad.some((datum) =>
203+
fetched.has(this.getDataLoadName(datum))
204+
);
205+
206+
if (shouldNotify) {
207+
this.onLoadFinish();
208+
}
209+
}
210+
211+
const isDataFetchPending = Array.from(
212+
this._dataLoadState.values()
213+
).some((loadState) => loadState === LoadState.LOADING);
214+
215+
if (!isDataFetchPending) {
216+
// Read-only property have a special setter.
217+
this._setDataLoading(false);
218+
}
219+
})
220+
);
221+
})
222+
);
189223
},
190224
};
191225
} // namespace tf_dashboard_common

0 commit comments

Comments
 (0)