Skip to content

Commit

Permalink
#745 Speedup resource history creation and provide feedback to user
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Dec 19, 2023
1 parent 42843fb commit f18ba72
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 81 deletions.
1 change: 1 addition & 0 deletions browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This changelog covers all three packages, as they are (for now) updated as a who
### @tomic/lib

- Always fetch all resources after setting + authenticating new agent with websockets #686
- Add progress callback to `resource.getHistory()` And increased its performance for resources with a large number of commits [#745](https://github.com/atomicdata-dev/atomic-server/issues/745)

## v0.36.1

Expand Down
42 changes: 42 additions & 0 deletions browser/data-browser/src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import styled from 'styled-components';

interface ProgressBarProps {
value: number;
}

export const ProgressBar: React.FC<ProgressBarProps> = ({ value }) => {
return <Progress value={value} max='100' />;
};

const Progress = styled.progress`
--progress-bg: ${p => p.theme.colors.bg1};
--progress-fg: ${p => p.theme.colors.main};
--progress-radius: 2rem;
--progress-height: 0.5rem;
flex: 1;
appearance: none;
// Needed for the border radius to work on chrome
overflow: hidden;
// Firefox
border-radius: var(--progress-radius);
height: var(--progress-height);
background-color: var(--progress-bg);
border: none;
&[value]::-moz-progress-bar {
background-color: var(--progress-fg);
}
// Chrome & Safari
&[value]::-webkit-progress-bar {
background-color: var(--progress-bg);
border-radius: var(--progress-radius);
height: var(--progress-height);
}
&[value]::-webkit-progress-value {
background-color: var(--progress-fg);
}
`;
25 changes: 23 additions & 2 deletions browser/data-browser/src/routes/History/HistoryRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { constructOpenURL } from '../../helpers/navigation';
import { HistoryDesktopView } from './HistoryDesktopView';
import { HistoryMobileView } from './HistoryMobileView';
import { useMediaQuery } from '../../hooks/useMediaQuery';
import { Column, Row } from '../../components/Row';
import { ProgressBar } from '../../components/ProgressBar';

/** Shows an activity log of previous versions */
export function History(): JSX.Element {
Expand All @@ -21,7 +23,7 @@ export function History(): JSX.Element {
const isSmallScreen = useMediaQuery('(max-width: 500px)');
const [subject] = useCurrentSubject();
const resource = useResource(subject);
const { versions, loading, error } = useVersions(resource);
const { versions, loading, error, progress } = useVersions(resource);
const [selectedVersion, setSelectedVersion] = useState<Version | undefined>();

const groupedVersions: {
Expand Down Expand Up @@ -67,7 +69,19 @@ export function History(): JSX.Element {
const isCurrentVersion = selectedVersion === versions[versions.length - 1];

if (loading) {
return <ContainerNarrow>Loading history of {subject}...</ContainerNarrow>;
return (
<ContainerNarrow>
<Centered>
<Column fullWidth>
<span>Building history of {resource.title}</span>
<Row center fullWidth>
<ProgressBar value={progress} />
<span>{progress}%</span>
</Row>
</Column>
</Centered>
</ContainerNarrow>
);
}

if (error) {
Expand Down Expand Up @@ -108,3 +122,10 @@ const SplitView = styled.main`
word-break: break-word;
}
`;

const Centered = styled.div`
display: grid;
place-items: center;
height: 100dvh;
min-width: 100%;
`;
8 changes: 5 additions & 3 deletions browser/data-browser/src/routes/History/VersionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ const VersionRow = styled(ButtonClean)<{ selected: boolean }>`
background-color: ${p => (p.selected ? p.theme.colors.main : 'transparent')};
color: ${p => (p.selected ? 'white' : p.theme.colors.text)};
border-radius: ${p => p.theme.radius};
contain: paint;
:hover,
:focus-visible {
background: ${p => (p.selected ? p.theme.colors.main : p.theme.colors.bg1)};
&:hover,
&:focus-visible {
background-color: ${p =>
p.selected ? p.theme.colors.main : p.theme.colors.bg1};
}
`;
44 changes: 30 additions & 14 deletions browser/data-browser/src/routes/History/useVersions.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
import { Resource, Version, useStore } from '@tomic/react';
import { useState, useEffect } from 'react';
import { Resource, Version, unknownSubject, useStore } from '@tomic/react';
import { useState, useEffect, useRef, useTransition } from 'react';
import { dedupeVersions } from './versionHelpers';

export interface UseVersionsResult {
versions: Version[];
loading: boolean;
progress: number;
error: Error | undefined;
}

export function useVersions(resource: Resource): UseVersionsResult {
const [versions, setVersions] = useState<Version[]>([]);
const [progress, setProgress] = useState(0);
const isRunning = useRef(false);
const store = useStore();
const [_, startTransition] = useTransition();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>(undefined);

useEffect(() => {
resource
.getHistory(store)
.then(history => {
setVersions(dedupeVersions(history));
})
.catch(e => {
setError(e);
})
.finally(() => {
setLoading(false);
});
if (resource.getSubject() === unknownSubject) {
return;
}

if (isRunning.current) {
return;
}

startTransition(() => {
(async () => {
try {
isRunning.current = true;
const history = await resource.getHistory(store, setProgress);
const dedupedVersions = dedupeVersions(history);
setVersions(dedupedVersions);
} catch (e) {
setError(e);
} finally {
setLoading(false);
isRunning.current = false;
}
})();
});
}, [resource]);

return { versions, loading, error };
return { versions, loading, error, progress };
}
55 changes: 43 additions & 12 deletions browser/data-browser/src/routes/History/versionHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,34 @@ const groupFormatter = new Intl.DateTimeFormat('default', {
year: 'numeric',
});

/** Removes back to back duplicate versions */
export function dedupeVersions(versions: Version[]): Version[] {
return versions.filter((v, i) => {
const filtered: Version[] = [];
let v: Version;
let prev: Version;

for (let i = 0; i < versions.length; i++) {
v = versions[i];

if (i === 0) {
return true;
filtered.push(v);
continue;
}

const prev = versions[i - 1];
prev = versions[i - 1];

if (v.commit.signer !== prev.commit.signer) {
return true;
filtered.push(v);
continue;
}

if (compareMaps(v.resource.getPropVals(), prev.resource.getPropVals())) {
continue;
}

return resourceToString(v.resource) !== resourceToString(prev.resource);
});
filtered.push(v);
}

return filtered;
}

export async function setResourceToVersion(
Expand Down Expand Up @@ -58,12 +71,30 @@ export function groupVersionsByMonth(
}, {});
}

function resourceToString(resource: Resource) {
const obj = {};
function compareMaps(map1: Map<string, unknown>, map2: Map<string, unknown>) {
// Reassigning to testVal uses less memory than redeclaring using const.
let testVal: unknown;

for (const [key, value] of resource.getPropVals().entries()) {
obj[key] = value;
if (map1.size !== map2.size) {
return false;
}

for (const [key, val] of map1) {
testVal = map2.get(key);

// in cases of an undefined value, make sure the key
// actually exists on the object so there are no false positives
if (testVal !== val || (testVal === undefined && !map2.has(key))) {
if (
Array.isArray(val) &&
JSON.stringify(val) === JSON.stringify(testVal)
) {
continue;
}

return false;
}
}

return JSON.stringify(obj);
return true;
}
23 changes: 18 additions & 5 deletions browser/lib/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,28 +295,39 @@ export class Resource<C extends OptionalClass = any> {
}

/** builds all versions using the Commits */
public async getHistory(store: Store): Promise<Version[]> {
public async getHistory(
store: Store,
progressCallback?: (percentage: number) => void,
): Promise<Version[]> {
const commitsCollection = await store.fetchResourceFromServer(
this.getCommitsCollection(),
);
const commits = commitsCollection.get(properties.collection.members);
const commits = commitsCollection.get(
properties.collection.members,
) as string[];

const builtVersions: Version[] = [];

let previousResource = new Resource(this.subject);

for (const commit of commits as unknown as string[]) {
const commitResource = await store.getResourceAsync(commit);
for (let i = 0; i < commits.length; i++) {
const commitResource = await store.getResourceAsync(commits[i]);
const parsedCommit = parseCommitResource(commitResource);
const builtResource = applyCommitToResource(
previousResource.clone(),
parsedCommit,
);
builtVersions.push({
commit: parsedCommit,
resource: builtResource.clone(),
resource: builtResource,
});
previousResource = builtResource;

// Every 30 cycles we report the progress
if (progressCallback && i % 30 === 0) {
progressCallback(Math.round((i / commits.length) * 100));
await WaitForImmediate();
}
}

return builtVersions;
Expand Down Expand Up @@ -698,3 +709,5 @@ export function proxyResource<C extends OptionalClass = any>(

return new Proxy(resource.__internalObject, {});
}

const WaitForImmediate = () => new Promise(resolve => setTimeout(resolve));
Loading

0 comments on commit f18ba72

Please sign in to comment.