Skip to content

Commit

Permalink
[dagit] Repair async graph layout for path-prefix (#7480)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellendag committed Apr 18, 2022
1 parent 39c853a commit b94eeb0
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 44 deletions.
1 change: 1 addition & 0 deletions js_modules/dagit/packages/app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const config = {
apolloLinks,
basePath: pathPrefix,
origin: process.env.REACT_APP_BACKEND_ORIGIN || document.location.origin,
staticPathRoot: `${pathPrefix}/`,
telemetryEnabled,
};

Expand Down
8 changes: 8 additions & 0 deletions js_modules/dagit/packages/core/src/app/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import * as React from 'react';

export type AppContextValue = {
// `basePath` is the root used for routing and GraphQL requests. In open source Dagit,
// this will also be the same as the root for static assets, `staticPathRoot`.
basePath: string;
rootServerURI: string;
// `staticPathRoot` is the root where static assets are served from. In open source Dagit,
// this value will be essentially the same as `basePath`, e.g. `${basePath}/`. Setting this
// value in context allows us to set __webpack_public_path__ for WebWorkers, thereby allowing
// WebWorkers to import other files.
staticPathRoot?: string;
telemetryEnabled: boolean;
};

export const AppContext = React.createContext<AppContextValue>({
basePath: '',
rootServerURI: '',
staticPathRoot: '/',
telemetryEnabled: false,
});
15 changes: 12 additions & 3 deletions js_modules/dagit/packages/core/src/app/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,23 @@ export interface AppProviderProps {
config: {
apolloLinks: ApolloLink[];
basePath?: string;
telemetryEnabled?: boolean;
headers?: {[key: string]: string};
origin: string;
staticPathRoot?: string;
telemetryEnabled?: boolean;
};
}

export const AppProvider: React.FC<AppProviderProps> = (props) => {
const {appCache, config} = props;
const {apolloLinks, basePath = '', headers = {}, origin, telemetryEnabled = false} = config;
const {
apolloLinks,
basePath = '',
headers = {},
origin,
staticPathRoot = '/',
telemetryEnabled = false,
} = config;

const graphqlPath = `${basePath}/graphql`;
const rootServerURI = `${origin}${basePath}`;
Expand Down Expand Up @@ -158,9 +166,10 @@ export const AppProvider: React.FC<AppProviderProps> = (props) => {
() => ({
basePath,
rootServerURI,
staticPathRoot,
telemetryEnabled,
}),
[basePath, rootServerURI, telemetryEnabled],
[basePath, rootServerURI, staticPathRoot, telemetryEnabled],
);

return (
Expand Down
69 changes: 38 additions & 31 deletions js_modules/dagit/packages/core/src/graph/asyncGraphLayout.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import memoize from 'lodash/memoize';
import React from 'react';

import {AppContext} from '../app/AppContext';
import {asyncMemoize} from '../app/Util';
import {GraphData} from '../asset-graph/Utils';
import {AssetGraphLayout, layoutAssetGraph} from '../asset-graph/layout';

import {ILayoutOp, layoutOpGraph, OpGraphLayout} from './layout';

// Loads the web worker using the Webpack loader `worker-loader`, specifying the import inline.
// This allows us to use web workers without ejecting from `create-react-app` (in order to use the
// config). We need both worker-loader (wraps the worker code) and babel-loader (transpiles from
// TypeScript to target ES5) in order to keep worker code in sync with our existing libraries.

const ASYNC_LAYOUT_SOLID_COUNT = 50;

// Op Graph
Expand All @@ -24,16 +20,19 @@ const _opLayoutCacheKey = (ops: ILayoutOp[], parentOp?: ILayoutOp) => {

export const getFullOpLayout = memoize(layoutOpGraph, _opLayoutCacheKey);

export const asyncGetFullOpLayout = asyncMemoize((ops: ILayoutOp[], parentOp?: ILayoutOp) => {
return new Promise<OpGraphLayout>((resolve) => {
const worker = new Worker(new URL('../workers/dagre_layout.worker', import.meta.url));
worker.addEventListener('message', (event) => {
resolve(event.data);
worker.terminate();
export const asyncGetFullOpLayout = asyncMemoize(
(ops: ILayoutOp[], parentOp?: ILayoutOp, staticPathRoot?: string) => {
return new Promise<OpGraphLayout>((resolve) => {
const worker = new Worker(new URL('../workers/dagre_layout.worker', import.meta.url));
worker.addEventListener('message', (event) => {
resolve(event.data);
worker.terminate();
});
worker.postMessage({type: 'layoutOpGraph', ops, parentOp, staticPathRoot});
});
worker.postMessage({type: 'layoutOpGraph', ops, parentOp});
});
}, _opLayoutCacheKey);
},
_opLayoutCacheKey,
);

// Asset Graph

Expand All @@ -43,16 +42,19 @@ const _assetLayoutCacheKey = (graphData: GraphData) => {

export const getFullAssetLayout = memoize(layoutAssetGraph, _assetLayoutCacheKey);

export const asyncGetFullAssetLayout = asyncMemoize((graphData: GraphData) => {
return new Promise<OpGraphLayout>((resolve) => {
const worker = new Worker(new URL('../workers/dagre_layout.worker', import.meta.url));
worker.addEventListener('message', (event) => {
resolve(event.data);
worker.terminate();
export const asyncGetFullAssetLayout = asyncMemoize(
(graphData: GraphData, staticPathRoot?: string) => {
return new Promise<AssetGraphLayout>((resolve) => {
const worker = new Worker(new URL('../workers/dagre_layout.worker', import.meta.url));
worker.addEventListener('message', (event) => {
resolve(event.data);
worker.terminate();
});
worker.postMessage({type: 'layoutAssetGraph', graphData, staticPathRoot});
});
worker.postMessage({type: 'layoutAssetGraph', graphData});
});
}, _assetLayoutCacheKey);
},
_assetLayoutCacheKey,
);

// Helper Hooks:
// - Automatically switch between sync and async loading strategies
Expand Down Expand Up @@ -97,60 +99,65 @@ const initialState: State = {

export function useOpLayout(ops: ILayoutOp[], parentOp?: ILayoutOp) {
const [state, dispatch] = React.useReducer(reducer, initialState);
const {staticPathRoot} = React.useContext(AppContext);

const cacheKey = _opLayoutCacheKey(ops, parentOp);
const runAsync = ops.length >= ASYNC_LAYOUT_SOLID_COUNT;

React.useEffect(() => {
async function runAsyncLayout() {
dispatch({type: 'loading'});
const layout = await asyncGetFullOpLayout(ops, parentOp);
const layout = await asyncGetFullOpLayout(ops, parentOp, staticPathRoot);
dispatch({
type: 'layout',
payload: {layout, cacheKey},
});
}

if (ops.length < ASYNC_LAYOUT_SOLID_COUNT) {
if (!runAsync) {
const layout = getFullOpLayout(ops, parentOp);
dispatch({type: 'layout', payload: {layout, cacheKey}});
} else {
void runAsyncLayout();
}
}, [cacheKey, ops, parentOp]);
}, [cacheKey, ops, parentOp, runAsync, staticPathRoot]);

return {
loading: state.loading || !state.layout || state.cacheKey !== cacheKey,
async: ops.length >= ASYNC_LAYOUT_SOLID_COUNT,
async: runAsync,
layout: state.layout as OpGraphLayout | null,
};
}

export function useAssetLayout(graphData: GraphData) {
const [state, dispatch] = React.useReducer(reducer, initialState);
const {staticPathRoot} = React.useContext(AppContext);

const cacheKey = _assetLayoutCacheKey(graphData);
const nodeCount = Object.keys(graphData.nodes).length;
const runAsync = nodeCount >= ASYNC_LAYOUT_SOLID_COUNT;

React.useEffect(() => {
async function runAsyncLayout() {
dispatch({type: 'loading'});
const layout = await asyncGetFullAssetLayout(graphData);
const layout = await asyncGetFullAssetLayout(graphData, staticPathRoot);
dispatch({
type: 'layout',
payload: {layout, cacheKey},
});
}

if (Object.keys(graphData.nodes).length < ASYNC_LAYOUT_SOLID_COUNT) {
if (!runAsync) {
const layout = getFullAssetLayout(graphData);
dispatch({type: 'layout', payload: {layout, cacheKey}});
} else {
void runAsyncLayout();
}
}, [cacheKey, graphData]);
}, [cacheKey, graphData, runAsync, staticPathRoot]);

return {
loading: state.loading || !state.layout || state.cacheKey !== cacheKey,
async: Object.keys(graphData.nodes).length >= ASYNC_LAYOUT_SOLID_COUNT,
async: runAsync,
layout: state.layout as AssetGraphLayout | null,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const PERMISSIONS_ALLOW_ALL: PermissionsFromJSON = {
const testValue: AppContextValue = {
basePath: '',
rootServerURI: '',
staticPathRoot: '/',
telemetryEnabled: false,
};

Expand Down
34 changes: 24 additions & 10 deletions js_modules/dagit/packages/core/src/workers/dagre_layout.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,30 @@
* try to remove it.
*/

import {layoutAssetGraph} from '../asset-graph/layout';
import {layoutOpGraph} from '../graph/layout';
const ctx: Worker = self as any;
self.addEventListener('message', (event) => {
const {data} = event;

ctx.addEventListener('message', (event) => {
if (event.data.type === 'layoutOpGraph') {
const {ops, parentOp} = event.data;
ctx.postMessage(layoutOpGraph(ops, parentOp));
} else if (event.data.type === 'layoutAssetGraph') {
const {graphData} = event.data;
ctx.postMessage(layoutAssetGraph(graphData));
// Before we attempt any imports, manually set the Webpack public path to the static path root.
// This allows us to import paths when a path-prefix value has been set.
if (data.staticPathRoot) {
__webpack_public_path__ = data.staticPathRoot;
}

switch (data.type) {
case 'layoutOpGraph': {
import('../graph/layout').then(({layoutOpGraph}) => {
const {ops, parentOp} = data;
self.postMessage(layoutOpGraph(ops, parentOp));
});
break;
}
case 'layoutAssetGraph': {
import('../asset-graph/layout').then(({layoutAssetGraph}) => {
const {graphData} = data;
self.postMessage(layoutAssetGraph(graphData));
});
}
}
});

export {};

0 comments on commit b94eeb0

Please sign in to comment.