Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 91 additions & 8 deletions src/actions/code.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,122 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { getProfile } from 'firefox-profiler/selectors/profile';
import { getBrowserConnection } from 'firefox-profiler/selectors/app';
import { getSymbolServerUrl } from 'firefox-profiler/selectors/url-state';
import { getSourceCodeCacheGeneration } from 'firefox-profiler/selectors/code';
import {
fetchSource,
type FetchSourceResult,
} from 'firefox-profiler/utils/fetch-source';
import { RegularExternalCommunicationDelegate } from 'firefox-profiler/utils/query-api';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
import { findAddressProofForFile } from 'firefox-profiler/profile-logic/profile-data';
import type {
Action,
ThunkAction,
SourceCodeLoadingError,
ApiQueryError,
DecodedInstruction,
IndexIntoSourceTable,
} from 'firefox-profiler/types';

// The `generation` argument of these is the source code cache generation from
// when the fetch started, see SourceCodeCacheState.

export function beginLoadingSourceCodeFromUrl(
sourceIndex: IndexIntoSourceTable,
url: string
url: string,
generation: number
): Action {
return { type: 'SOURCE_CODE_LOADING_BEGIN_URL', sourceIndex, url };
return {
type: 'SOURCE_CODE_LOADING_BEGIN_URL',
sourceIndex,
url,
generation,
};
}

export function beginLoadingSourceCodeFromBrowserConnection(
sourceIndex: IndexIntoSourceTable
sourceIndex: IndexIntoSourceTable,
generation: number
): Action {
return { type: 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION', sourceIndex };
return {
type: 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION',
sourceIndex,
generation,
};
}

export function finishLoadingSourceCode(
sourceIndex: IndexIntoSourceTable,
code: string
code: string,
generation: number
): Action {
return { type: 'SOURCE_CODE_LOADING_SUCCESS', sourceIndex, code };
return { type: 'SOURCE_CODE_LOADING_SUCCESS', sourceIndex, code, generation };
}

export function failLoadingSourceCode(
sourceIndex: IndexIntoSourceTable,
errors: SourceCodeLoadingError[]
errors: SourceCodeLoadingError[],
generation: number
): Action {
return { type: 'SOURCE_CODE_LOADING_ERROR', sourceIndex, errors };
return { type: 'SOURCE_CODE_LOADING_ERROR', sourceIndex, errors, generation };
}

/**
* Fetch the contents of a source file into the source code cache, from the
* browser, a symbol server or the web, whichever can serve it.
*
* This always fetches, so callers need to skip sources that are already in the
* cache or embedded in the profile. Callers should keep one archiveCache across
* their fetches, so that each archive is only downloaded once.
*/
export function fetchSourceCode(
sourceIndex: IndexIntoSourceTable,
archiveCache: Map<string, Promise<Uint8Array>>
): ThunkAction<Promise<FetchSourceResult>> {
return async (dispatch, getState) => {
const state = getState();
const { sources, stringArray } = getProfile(state).shared;
// Read once here, so that the reducer can drop everything this fetch
// dispatches after another profile got loaded.
const generation = getSourceCodeCacheGeneration(state);
const delegate = new RegularExternalCommunicationDelegate(
getBrowserConnection(state),
{
onBeginUrlRequest: (url) => {
dispatch(beginLoadingSourceCodeFromUrl(sourceIndex, url, generation));
},
onBeginBrowserConnectionQuery: () => {
dispatch(
beginLoadingSourceCodeFromBrowserConnection(sourceIndex, generation)
);
},
}
);
const result = await fetchSource(
stringArray[sources.filename[sourceIndex]],
sources.id[sourceIndex],
getSymbolServerUrl(state),
findAddressProofForFile(getProfile(state), sourceIndex),
archiveCache,
delegate
);
switch (result.type) {
case 'SUCCESS':
dispatch(
finishLoadingSourceCode(sourceIndex, result.source, generation)
);
break;
case 'ERROR':
dispatch(failLoadingSourceCode(sourceIndex, result.errors, generation));
break;
default:
throw assertExhaustiveCheck(result);
}
return result;
};
}

export function beginLoadingAssemblyCodeFromUrl(
Expand Down
92 changes: 6 additions & 86 deletions src/components/app/SourceCodeFetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,27 @@
import React from 'react';

import {
getProfileOrNull,
getSourceViewCode,
getBrowserConnection,
getSymbolServerUrl,
getSourceViewFile,
getSourceViewSourceIndex,
getSourceViewSourceId,
} from 'firefox-profiler/selectors';
import {
beginLoadingSourceCodeFromUrl,
beginLoadingSourceCodeFromBrowserConnection,
finishLoadingSourceCode,
failLoadingSourceCode,
} from 'firefox-profiler/actions/code';
import { fetchSource } from 'firefox-profiler/utils/fetch-source';
import { RegularExternalCommunicationDelegate } from 'firefox-profiler/utils/query-api';
import { findAddressProofForFile } from 'firefox-profiler/profile-logic/profile-data';
import type { BrowserConnection } from 'firefox-profiler/app-logic/browser-connection';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
import { fetchSourceCode } from 'firefox-profiler/actions/code';
import explicitConnect from 'firefox-profiler/utils/connect';

import type { ConnectedProps } from 'firefox-profiler/utils/connect';
import type {
SourceCodeStatus,
Profile,
IndexIntoSourceTable,
} from 'firefox-profiler/types';

type StateProps = {
readonly sourceViewFile: string | null;
readonly sourceViewSourceIndex: IndexIntoSourceTable | null;
readonly sourceViewSourceId: string | null;
readonly sourceViewCode: SourceCodeStatus | void;
readonly symbolServerUrl: string;
readonly profile: Profile | null;
readonly browserConnection: BrowserConnection | null;
};

type DispatchProps = {
readonly beginLoadingSourceCodeFromUrl: typeof beginLoadingSourceCodeFromUrl;
readonly beginLoadingSourceCodeFromBrowserConnection: typeof beginLoadingSourceCodeFromBrowserConnection;
readonly finishLoadingSourceCode: typeof finishLoadingSourceCode;
readonly failLoadingSourceCode: typeof failLoadingSourceCode;
readonly fetchSourceCode: typeof fetchSourceCode;
};

type Props = ConnectedProps<{}, StateProps, DispatchProps>;
Expand All @@ -63,68 +41,19 @@ class SourceCodeFetcherImpl extends React.PureComponent<Props> {
this._triggerSourceLoadingIfNeeded();
}

async _triggerSourceLoadingIfNeeded() {
_triggerSourceLoadingIfNeeded() {
const {
sourceViewSourceIndex,
sourceViewCode,
sourceViewFile,
sourceViewSourceId,
beginLoadingSourceCodeFromUrl,
beginLoadingSourceCodeFromBrowserConnection,
finishLoadingSourceCode,
failLoadingSourceCode,
symbolServerUrl,
profile,
browserConnection,
fetchSourceCode,
} = this.props;

if (sourceViewSourceIndex === null || sourceViewCode || !sourceViewFile) {
return;
}

if (!profile) {
return;
}

const addressProof = findAddressProofForFile(
profile,
sourceViewSourceIndex
);

const delegate = new RegularExternalCommunicationDelegate(
browserConnection,
{
onBeginUrlRequest: (url: string) => {
beginLoadingSourceCodeFromUrl(sourceViewSourceIndex, url);
},
onBeginBrowserConnectionQuery: () => {
beginLoadingSourceCodeFromBrowserConnection(sourceViewSourceIndex);
},
}
);

const fetchSourceResult = await fetchSource(
sourceViewFile,
sourceViewSourceId,
symbolServerUrl,
addressProof,
this._archiveCache,
delegate
);

switch (fetchSourceResult.type) {
case 'SUCCESS':
finishLoadingSourceCode(
sourceViewSourceIndex,
fetchSourceResult.source
);
break;
case 'ERROR':
failLoadingSourceCode(sourceViewSourceIndex, fetchSourceResult.errors);
break;
default:
throw assertExhaustiveCheck(fetchSourceResult);
}
fetchSourceCode(sourceViewSourceIndex, this._archiveCache);
}

override render() {
Expand All @@ -137,18 +66,9 @@ export const SourceCodeFetcher = explicitConnect<{}, StateProps, DispatchProps>(
mapStateToProps: (state) => ({
sourceViewSourceIndex: getSourceViewSourceIndex(state),
sourceViewFile: getSourceViewFile(state),
sourceViewSourceId: getSourceViewSourceId(state),
sourceViewCode: getSourceViewCode(state),
symbolServerUrl: getSymbolServerUrl(state),
profile: getProfileOrNull(state),
browserConnection: getBrowserConnection(state),
}),
mapDispatchToProps: {
beginLoadingSourceCodeFromUrl,
beginLoadingSourceCodeFromBrowserConnection,
finishLoadingSourceCode,
failLoadingSourceCode,
},
mapDispatchToProps: { fetchSourceCode },
component: SourceCodeFetcherImpl,
}
);
78 changes: 33 additions & 45 deletions src/reducers/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,56 @@
import type {
Reducer,
SourceCodeStatus,
SourceCodeCacheState,
SourcesAction,
AssemblyCodeStatus,
CodeState,
IndexIntoSourceTable,
} from 'firefox-profiler/types';
import { combineReducers } from 'redux';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';

const sourceCodeCache: Reducer<Map<IndexIntoSourceTable, SourceCodeStatus>> = (
state = new Map(),
const sourceCodeCache: Reducer<SourceCodeCacheState> = (
state = { generation: 0, statuses: new Map() },
action
) => {
switch (action.type) {
case 'SOURCE_CODE_LOADING_BEGIN_URL': {
const { sourceIndex, url } = action;
const newState = new Map(state);
newState.set(sourceIndex, {
type: 'LOADING',
source: { type: 'URL', url },
});
return newState;
}
case 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION': {
const { sourceIndex } = action;
const newState = new Map(state);
newState.set(sourceIndex, {
type: 'LOADING',
source: { type: 'BROWSER_CONNECTION' },
});
return newState;
}
case 'SOURCE_CODE_LOADING_SUCCESS': {
const { sourceIndex, code } = action;
const newState = new Map(state);
newState.set(sourceIndex, { type: 'AVAILABLE', code });
return newState;
}
case 'SOURCE_CODE_LOADING_BEGIN_URL':
case 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION':
case 'SOURCE_CODE_LOADING_SUCCESS':
case 'SOURCE_CODE_LOADING_ERROR': {
const { sourceIndex, errors } = action;
const newState = new Map(state);
newState.set(sourceIndex, { type: 'ERROR', errors });
return newState;
}
case 'SANITIZED_PROFILE_PUBLISHED': {
if (!action.translationMaps) {
if (action.generation !== state.generation) {
// The fetch started for another profile, so its sourceIndex may point
// at a different file now. This includes the LOADING states, which
// nothing would ever finish.
return state;
}
const { oldSourceToNewSourcePlusOne } = action.translationMaps;
const newState = new Map();
for (const [sourceIndex, status] of state) {
const newSourceIndexPlusOne = oldSourceToNewSourcePlusOne[sourceIndex];
if (newSourceIndexPlusOne === 0) {
continue;
}
const newSourceIndex = newSourceIndexPlusOne - 1;
newState.set(newSourceIndex, status);
}
return newState;
const statuses = new Map(state.statuses);
statuses.set(action.sourceIndex, _getSourceCodeStatus(action));
return { ...state, statuses };
}
case 'PROFILE_LOADED':
// The source indexes only mean something for the profile they came from.
return { generation: state.generation + 1, statuses: new Map() };
default:
return state;
}
};

function _getSourceCodeStatus(action: SourcesAction): SourceCodeStatus {
switch (action.type) {
case 'SOURCE_CODE_LOADING_BEGIN_URL':
return { type: 'LOADING', source: { type: 'URL', url: action.url } };
case 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION':
return { type: 'LOADING', source: { type: 'BROWSER_CONNECTION' } };
case 'SOURCE_CODE_LOADING_SUCCESS':
return { type: 'AVAILABLE', code: action.code };
case 'SOURCE_CODE_LOADING_ERROR':
return { type: 'ERROR', errors: action.errors };
default:
throw assertExhaustiveCheck(action);
}
}

const assemblyCodeCache: Reducer<Map<string, AssemblyCodeStatus>> = (
state = new Map(),
action
Expand Down
5 changes: 4 additions & 1 deletion src/selectors/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { getProfileOrNull } from './profile';

export const getSourceCodeCache: Selector<
Map<IndexIntoSourceTable, SourceCodeStatus>
> = (state) => state.code.sourceCodeCache;
> = (state) => state.code.sourceCodeCache.statuses;

export const getSourceCodeCacheGeneration: Selector<number> = (state) =>
state.code.sourceCodeCache.generation;

export const getSourceViewCode: Selector<SourceCodeStatus | void> =
createSelector(
Expand Down
Loading
Loading