Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Maps][File upload] Parse geojson files in chunks to avoid thread blocking #46710

Merged
merged 43 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0cf9228
Add file parse chunking, update component on progress
Sep 24, 2019
0f9312b
Clean up clean and validate and redo to process single features
Sep 24, 2019
bf7e12e
Add oboe dependency
Sep 24, 2019
63b7226
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Sep 24, 2019
babf21c
Prevent state updates on cancel
Sep 25, 2019
e46c240
Handle new files added mid-way through parsing another file
Sep 25, 2019
0688e73
Fix issue where subsequent index name is wiped out when previous file…
Sep 25, 2019
ab97b97
Remove unneeded oboe abort logic
Sep 25, 2019
a02c37e
Dice parsing logic up further for testing
Sep 25, 2019
196d28f
Clean up
Sep 26, 2019
879b5ac
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 1, 2019
266a8a4
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 3, 2019
13dead5
Revert "Fix issue where subsequent index name is wiped out when previ…
Oct 3, 2019
ac45a72
Update file parse test to focus on different stream states
Oct 4, 2019
325d1f5
Update clean and validate tests to reflect function input/output changes
Oct 7, 2019
a939200
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 7, 2019
d1fe56a
Bump up file buffer. Simplify ui update logic, not neceesary to throt…
Oct 7, 2019
1736920
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 7, 2019
6650a75
Show features parsed on UI rather than percentage
Oct 8, 2019
fa57417
Remove extra mock reset
Oct 8, 2019
e564692
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 8, 2019
36311bb
Review feedback. Add localized feature tracking callback
Oct 8, 2019
da7fda9
Review feedback. Add comment explaining progress update throttling. A…
Oct 8, 2019
53f59b3
Remove console log
Oct 8, 2019
102efc2
Consolidate feature handling into one function passed to oboeStream node
Oct 10, 2019
1e641eb
Abstract oboe logic to separate class and import for use in file parser
Oct 10, 2019
171402d
Update file parser test to mock PatternReader import
Oct 10, 2019
0aaf38e
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 11, 2019
267755e
Prevent file parse active flag from resetting if another file is in p…
Oct 11, 2019
762472d
Don't pass back result if no features found on complete, throw error …
Oct 11, 2019
0c04637
Use singleton version of jsts reader & writer. Pass back unmodified f…
Oct 11, 2019
8084062
Make fileHandler function async
Oct 11, 2019
aa4f986
Return null if no geometry
Oct 11, 2019
9e6505d
Handle single features differently. Fixes functional test error
Oct 11, 2019
790abe7
Update jest test to use unique instances & counts of readers
Oct 14, 2019
1b7b0fb
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 14, 2019
7051a73
Review feedback
Oct 14, 2019
8e2ae68
Merge branch 'master' of github.com:elastic/kibana into geojson-file-…
Oct 15, 2019
2954061
Review feedback
Oct 15, 2019
33ca43f
Review feedback. Add error-handling for null geom
Oct 15, 2019
a0afe21
Fix i18n error
Oct 15, 2019
a78f09d
Clean up handling of cancelled/replaced files to account for changed …
Oct 15, 2019
95a485e
Merge remote-tracking branch 'upstream/master' into geojson-file-chun…
Oct 15, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { parseFile } from '../util/file_parser';
import { MAX_FILE_SIZE } from '../../common/constants/file_import';
import _ from 'lodash';

const ACCEPTABLE_FILETYPES = [
'json',
Expand All @@ -20,7 +21,10 @@ export class JsonIndexFilePicker extends Component {

state = {
fileUploadError: '',
fileParsingProgress: '',
percentageProcessed: 0,
featuresProcessed: 0,
fileParseActive: false,
currentFileTracker: null,
};

async componentDidMount() {
Expand All @@ -31,16 +35,28 @@ export class JsonIndexFilePicker extends Component {
this._isMounted = false;
}

getFileParseActive = () => this._isMounted && this.state.fileParseActive;

_fileHandler = fileList => {
const fileArr = Array.from(fileList);
this.props.resetFileAndIndexSettings();
this.setState({ fileUploadError: '' });
this.setState({
fileUploadError: '',
percentageProcessed: 0,
featuresProcessed: 0,
});
if (fileArr.length === 0) { // Remove
this.setState({
fileParseActive: false
});
return;
}
const file = fileArr[0];

this._parseFile(file);
this.setState({
fileParseActive: true,
currentFileTracker: Symbol()
}, () => this._parseFile(file));
};

_checkFileSize = ({ size }) => {
Expand Down Expand Up @@ -106,7 +122,17 @@ export class JsonIndexFilePicker extends Component {
return fileNameOnly.toLowerCase();
}

// It's necessary to throttle progress. Updates that are too frequent cause
// issues (update failure) in the nested progress component
setFileProgress = _.debounce(({ featuresProcessed, bytesProcessed, totalBytes }) => {
const percentageProcessed = parseInt((100 * bytesProcessed) / totalBytes);
if (this.getFileParseActive()) {
this.setState({ featuresProcessed, percentageProcessed });
}
}, 150);

async _parseFile(file) {
const { currentFileTracker } = this.state;
const {
setFileRef, setParsedFile, resetFileAndIndexSettings, onFileUpload,
transformDetails, setIndexName
Expand All @@ -119,15 +145,19 @@ export class JsonIndexFilePicker extends Component {
return;
}
// Parse file
this.setState({ fileParsingProgress: i18n.translate(
'xpack.fileUpload.jsonIndexFilePicker.parsingFile',
{ defaultMessage: 'Parsing file...' })
});
const parsedFileResult = await parseFile(
file, transformDetails, onFileUpload
).catch(err => {

const fileResult = await parseFile({
file,
transformDetails,
onFileUpload,
setFileProgress: this.setFileProgress,
getFileParseActive: this.getFileParseActive
}).catch(err => {
if (this._isMounted) {
this.setState({
fileParseActive: false,
percentageProcessed: 0,
featuresProcessed: 0,
fileUploadError: (
<FormattedMessage
id="xpack.fileUpload.jsonIndexFilePicker.unableParseFile"
Expand All @@ -143,25 +173,59 @@ export class JsonIndexFilePicker extends Component {
if (!this._isMounted) {
return;
}
this.setState({ fileParsingProgress: '' });
if (!parsedFileResult) {

// If another file is replacing this one, leave file parse active
this.setState({
percentageProcessed: 0,
featuresProcessed: 0,
fileParseActive: currentFileTracker !== this.state.currentFileTracker
});
if (!fileResult) {
resetFileAndIndexSettings();
return;
}
const { errors, parsedGeojson } = fileResult;

if (errors.length) {
// Set only the first error for now (since there's only one).
// TODO: Add handling in case of further errors
const error = errors[0];
this.setState({
fileUploadError: (
<FormattedMessage
id="xpack.fileUpload.jsonIndexFilePicker.fileParseError"
defaultMessage="File parse error(s) detected: {error}"
values={{ error }}
/>
)
});
}
setIndexName(defaultIndexName);
setFileRef(file);
setParsedFile(parsedFileResult);
setParsedFile(parsedGeojson);
}



render() {
const { fileParsingProgress, fileUploadError } = this.state;
const {
fileUploadError,
percentageProcessed,
featuresProcessed,
} = this.state;

return (
<Fragment>
{fileParsingProgress ? <EuiProgress size="xs" color="accent" position="absolute" /> : null}
{percentageProcessed
? <EuiProgress
value={percentageProcessed}
max={100}
size="xs"
color="accent"
position="absolute"
/>
: null
}
<EuiFormRow
label={
<FormattedMessage
Expand All @@ -172,8 +236,14 @@ export class JsonIndexFilePicker extends Component {
isInvalid={fileUploadError !== ''}
error={[fileUploadError]}
helpText={
fileParsingProgress ? (
fileParsingProgress
percentageProcessed ? (
i18n.translate(
'xpack.fileUpload.jsonIndexFilePicker.parsingFile', {
defaultMessage: '{featuresProcessed} features parsed...',
values: {
featuresProcessed
}
})
) : (
<span>
{i18n.translate('xpack.fileUpload.jsonIndexFilePicker.formatsAccepted', {
Expand Down
146 changes: 117 additions & 29 deletions x-pack/legacy/plugins/file_upload/public/util/file_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,127 @@
import _ from 'lodash';
import { geoJsonCleanAndValidate } from './geo_json_clean_and_validate';
import { i18n } from '@kbn/i18n';
import { PatternReader } from './pattern_reader';

export async function readFile(file) {
const readPromise = new Promise((resolve, reject) => {
if (!file) {
reject(new Error(i18n.translate(
'xpack.fileUpload.fileParser.noFileProvided', {
// In local testing, performance improvements leveled off around this size
export const FILE_BUFFER = 1024000;

const readSlice = (fileReader, file, start, stop) => {
const blob = file.slice(start, stop);
fileReader.readAsBinaryString(blob);
};

let prevFileReader;
let prevPatternReader;
export const fileHandler = async ({
kindsun marked this conversation as resolved.
Show resolved Hide resolved
file,
setFileProgress,
cleanAndValidate,
getFileParseActive,
fileReader = new FileReader()
}) => {

if (!file) {
return Promise.reject(
new Error(
i18n.translate('xpack.fileUpload.fileParser.noFileProvided', {
defaultMessage: 'Error, no file provided',
})));
}
const fr = new window.FileReader();
fr.onload = e => resolve(e.target.result);
fr.onerror = () => {
fr.abort();
})
)
);
}


// Halt any previous file reading & pattern checking activity
if (prevFileReader) {
prevFileReader.abort();
}
if (prevPatternReader) {
prevPatternReader.abortStream();
}

// Set up feature tracking
let featuresProcessed = 0;
const onFeatureRead = feature => {
// TODO: Add handling and tracking for cleanAndValidate fails
featuresProcessed++;
return cleanAndValidate(feature);
};

let start;
let stop = FILE_BUFFER;

prevFileReader = fileReader;

const filePromise = new Promise((resolve, reject) => {
const onStreamComplete = fileResults => {
if (!featuresProcessed) {
reject(
new Error(
i18n.translate('xpack.fileUpload.fileParser.noFeaturesDetected', {
defaultMessage: 'Error, no features detected',
})
)
);
} else {
resolve(fileResults);
}
};
const patternReader = new PatternReader({ onFeatureDetect: onFeatureRead, onStreamComplete });
prevPatternReader = patternReader;

fileReader.onloadend = ({ target: { readyState, result } }) => {
if (readyState === FileReader.DONE) {
if (!getFileParseActive() || !result) {
fileReader.abort();
patternReader.abortStream();
resolve(null);
return;
}
setFileProgress({
featuresProcessed,
bytesProcessed: stop || file.size,
totalBytes: file.size
});
patternReader.writeDataToPatternStream(result);
if (!stop) {
return;
}

start = stop;
const newStop = stop + FILE_BUFFER;
// Check EOF
stop = newStop > file.size ? undefined : newStop;
readSlice(fileReader, file, start, stop);
}
};
fileReader.onerror = () => {
fileReader.abort();
patternReader.abortStream();
reject(new Error(i18n.translate(
'xpack.fileUpload.fileParser.errorReadingFile', {
defaultMessage: 'Error reading file',
})));
};
fr.readAsText(file);
});
readSlice(fileReader, file, start, stop);
return filePromise;
};

return await readPromise;
}

export function jsonPreview(json, previewFunction) {
// Call preview (if any)
if (json && previewFunction) {
const defaultName = _.get(json, 'name', 'Import File');
previewFunction(json, defaultName);
export function jsonPreview(fileResults, previewFunction) {
if (fileResults && fileResults.parsedGeojson && previewFunction) {
const defaultName = _.get(fileResults.parsedGeojson, 'name', 'Import File');
previewFunction(fileResults.parsedGeojson, defaultName);
}
}

export async function parseFile(file, transformDetails, previewCallback = null) {
export async function parseFile({
file,
transformDetails,
onFileUpload: previewCallback = null,
setFileProgress,
getFileParseActive
}) {
let cleanAndValidate;
if (typeof transformDetails === 'object') {
cleanAndValidate = transformDetails.cleanAndValidate;
Expand All @@ -56,15 +144,15 @@ export async function parseFile(file, transformDetails, previewCallback = null)
values: { transformDetails }
})
);
return;
}
}

const rawResults = await readFile(file);
const parsedJson = JSON.parse(rawResults);
const jsonResult = cleanAndValidate(parsedJson);
jsonPreview(jsonResult, previewCallback);

return jsonResult;
const fileResults = await fileHandler({
file,
setFileProgress,
cleanAndValidate,
getFileParseActive
});
jsonPreview(fileResults, previewCallback);
return fileResults;
}

Loading