Skip to content

Commit a5a87eb

Browse files
authored
improvement: improve performance of bit-import on lane (teambit#5931)
- import only "delta" when on a lane with the same way it is done for main. meaning, send to the server the current hash and only fetch if the server has a newer hash, in which case, fetch the delta between the two. (disabled temporarily, until the server is deployed with these changes). - in case the components exist locally but their version are unbuilt, avoid fetching them again from the remote with all their dependencies, unless their `builtStatus` has changed and is now successful.
1 parent 3f5c727 commit a5a87eb

7 files changed

Lines changed: 111 additions & 29 deletions

File tree

e2e/harmony/sign.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe('sign command', function () {
117117
});
118118
describe('running bit import on the workspace', () => {
119119
before(() => {
120-
helper.command.importAllComponents();
120+
helper.command.import('--all-history');
121121
});
122122
it('should bring the updated Version from the remote', () => {
123123
const comp1 = helper.command.catComponent(`${helper.scopes.remote}/bar@latest`);

src/api/scope/lib/fetch.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Readable } from 'stream';
22
import { LaneId } from '@teambit/lane-id';
33
import { BitIds } from '../../../bit-id';
4-
import { POST_SEND_OBJECTS, PRE_SEND_OBJECTS } from '../../../constants';
4+
import { LATEST_BIT_VERSION, POST_SEND_OBJECTS, PRE_SEND_OBJECTS } from '../../../constants';
55
import HooksManager from '../../../hooks';
66
import logger from '../../../logger/logger';
77
import { loadScope, Scope } from '../../../scope';
@@ -13,13 +13,16 @@ import {
1313
ObjectsReadableGenerator,
1414
} from '../../../scope/objects/objects-readable-generator';
1515
import { LaneNotFound } from './exceptions/lane-not-found';
16+
import { Lane } from '../../../scope/models';
1617

1718
export type FETCH_TYPE = 'component' | 'lane' | 'object' | 'component-delta';
1819
export type FETCH_OPTIONS = {
1920
type: FETCH_TYPE;
2021
withoutDependencies: boolean; // default - true
2122
includeArtifacts: boolean; // default - false
2223
allowExternal: boolean; // allow fetching components of other scope from this scope. needed for lanes.
24+
laneId?: string; // relevant for "component-delta" type to know where to find the component latest
25+
onlyIfBuilt?: boolean; // relevant when fetching with deps. if true, and the component wasn't built successfully, don't fetch it.
2326
};
2427

2528
const HooksManagerInstance = HooksManager.getInstance();
@@ -50,7 +53,7 @@ export default async function fetch(
5053
switch (fetchOptions.type) {
5154
case 'component': {
5255
const bitIds: BitIds = BitIds.deserialize(ids);
53-
const { withoutDependencies, includeArtifacts, allowExternal } = fetchOptions;
56+
const { withoutDependencies, includeArtifacts, allowExternal, onlyIfBuilt } = fetchOptions;
5457
const collectParents = !withoutDependencies;
5558
const scopeComponentsImporter = ScopeComponentsImporter.getInstance(scope);
5659
const getComponentsWithOptions = async (): Promise<ComponentWithCollectOptions[]> => {
@@ -63,7 +66,7 @@ export default async function fetch(
6366
collectParents,
6467
}));
6568
}
66-
const versionsDependencies = await scopeComponentsImporter.fetchWithDeps(bitIds, allowExternal);
69+
const versionsDependencies = await scopeComponentsImporter.fetchWithDeps(bitIds, allowExternal, onlyIfBuilt);
6770
return versionsDependencies
6871
.map((versionDep) => [
6972
{
@@ -89,7 +92,9 @@ export default async function fetch(
8992
case 'component-delta': {
9093
const bitIdsWithHashToStop: BitIds = BitIds.deserialize(ids);
9194
const scopeComponentsImporter = ScopeComponentsImporter.getInstance(scope);
92-
const bitIdsLatest = bitIdsWithHashToStop.toVersionLatest();
95+
const laneId = fetchOptions.laneId ? LaneId.parse(fetchOptions.laneId) : null;
96+
const lane = laneId ? await scope.loadLane(laneId) : null;
97+
const bitIdsLatest = bitIdsToLatest(bitIdsWithHashToStop, lane);
9398
const importedComponents = await scopeComponentsImporter.fetchWithoutDeps(
9499
bitIdsLatest,
95100
fetchOptions.allowExternal
@@ -151,3 +156,16 @@ export default async function fetch(
151156
logger.debug('scope.fetch returns readable');
152157
return objectsReadableGenerator.readable;
153158
}
159+
160+
function bitIdsToLatest(bitIds: BitIds, lane: Lane | null) {
161+
if (!lane) {
162+
return bitIds.toVersionLatest();
163+
}
164+
const laneIds = lane.toBitIds();
165+
return BitIds.fromArray(
166+
bitIds.map((bitId) => {
167+
const inLane = laneIds.searchWithoutVersion(bitId);
168+
return inLane || bitId.changeVersion(LATEST_BIT_VERSION);
169+
})
170+
);
171+
}

src/consumer/component-ops/import-components.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,14 @@ export default class ImportComponents {
7979
options: ImportOptions;
8080
// @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
8181
mergeStatus: { [id: string]: FilesStatus };
82-
private laneObjects: Lane[] = [];
82+
private laneObjects: Lane[];
8383
private divergeData: Array<ModelComponent> = [];
8484
// @ts-ignore
8585
constructor(consumer: Consumer, options: ImportOptions) {
8686
this.consumer = consumer;
8787
this.scope = consumer.scope;
8888
this.options = options;
89+
this.laneObjects = this.options.lanes ? (this.options.lanes.lanes as Lane[]) : [];
8990
}
9091

9192
importComponents(): Promise<ImportResult> {
@@ -96,10 +97,49 @@ export default class ImportComponents {
9697
this.options.installNpmPackages = false;
9798
this.options.saveDependenciesAsComponents = true;
9899
}
99-
if (!this.options.lanes && (!this.options.ids || R.isEmpty(this.options.ids))) {
100-
return this.importAccordingToBitMap();
100+
if (this.options.lanes && !this.options.ids.length) {
101+
// @todo: uncomment this once the code is deployed to the server
102+
// return this.importObjectsOnLane();
103+
return this.importSpecificComponents();
101104
}
102-
return this.importSpecificComponents();
105+
if (this.options.ids.length) {
106+
return this.importSpecificComponents();
107+
}
108+
return this.importAccordingToBitMap();
109+
}
110+
111+
async importObjectsOnLane(): Promise<ImportResult> {
112+
if (!this.laneObjects.length || !this.options.objectsOnly || this.consumer.isLegacy) {
113+
throw new Error(`importObjectsOnLane should work on Harmony and have laneObjects and objectsOnly=true`);
114+
}
115+
if (this.laneObjects.length > 1) {
116+
throw new Error(`importObjectsOnLane does not support more than one lane`);
117+
}
118+
const lane = this.laneObjects[0];
119+
const bitIds: BitIds = await this.getBitIds();
120+
logger.debug(`importObjectsOnLane, Lane: ${lane.id()}, Ids: ${bitIds.toString()}`);
121+
const beforeImportVersions = await this._getCurrentVersions(bitIds);
122+
const componentsWithDependencies = await this.consumer.importComponentsObjectsHarmony(
123+
bitIds,
124+
false,
125+
this.options.allHistory,
126+
lane
127+
);
128+
129+
// merge the lane objects
130+
const mergeAllLanesResults = await pMapSeries(this.laneObjects, (laneObject) =>
131+
this.scope.sources.mergeLane(laneObject, true)
132+
);
133+
const mergedLanes = mergeAllLanesResults.map((result) => result.mergeLane);
134+
await Promise.all(mergedLanes.map((mergedLane) => this.scope.lanes.saveLane(mergedLane)));
135+
136+
const componentsWithDependenciesFiltered = this._filterComponentsWithLowerVersions(componentsWithDependencies);
137+
await this._fetchDivergeData(componentsWithDependenciesFiltered);
138+
this._throwForDivergedHistory();
139+
await this._writeToFileSystem(componentsWithDependenciesFiltered);
140+
await this._saveLaneDataIfNeeded(componentsWithDependenciesFiltered);
141+
const importDetails = await this._getImportDetails(beforeImportVersions, componentsWithDependencies);
142+
return { dependencies: componentsWithDependenciesFiltered, importDetails };
103143
}
104144

105145
async importSpecificComponents(): Promise<ImportResult> {
@@ -187,7 +227,6 @@ export default class ImportComponents {
187227
if (!this.options.lanes) {
188228
throw new Error(`getBitIdsForLanes: this.options.lanes must be set`);
189229
}
190-
this.laneObjects = this.options.lanes.lanes as Lane[];
191230
const bitIdsFromLane = BitIds.fromArray(this.laneObjects.flatMap((lane) => lane.toBitIds()));
192231

193232
if (!this.options.ids.length) {

src/consumer/consumer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,14 +434,15 @@ export default class Consumer {
434434
async importComponentsObjectsHarmony(
435435
ids: BitIds,
436436
fromOriginalScope = false,
437-
allHistory = false
437+
allHistory = false,
438+
lane?: Lane
438439
): Promise<ComponentWithDependencies[]> {
439440
const scopeComponentsImporter = ScopeComponentsImporter.getInstance(this.scope);
440-
await scopeComponentsImporter.importManyDeltaWithoutDeps(ids, allHistory);
441+
await scopeComponentsImporter.importManyDeltaWithoutDeps(ids, allHistory, lane);
441442
loader.start(`import ${ids.length} components with their dependencies (if missing)`);
442443
const versionDependenciesArr: VersionDependencies[] = fromOriginalScope
443444
? await scopeComponentsImporter.importManyFromOriginalScopes(ids)
444-
: await scopeComponentsImporter.importMany({ ids });
445+
: await scopeComponentsImporter.importMany({ ids, lanes: lane ? [lane] : undefined });
445446
const componentWithDependencies = await multipleVersionDependenciesToConsumer(
446447
versionDependenciesArr,
447448
this.scope.objects

src/scope/component-ops/scope-components-importer.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import VersionDependencies from '../version-dependencies';
2828
import { BitObjectList } from '../objects/bit-object-list';
2929
import { ObjectFetcher } from '../objects-fetcher/objects-fetcher';
3030
import { concurrentComponentsLimit } from '../../utils/concurrency';
31+
import { BuildStatus } from '../../constants';
3132

3233
const removeNils = R.reject(R.isNil);
3334

@@ -82,9 +83,10 @@ export default class ScopeComponentsImporter {
8283
}): Promise<VersionDependencies[]> {
8384
logger.debugAndAddBreadCrumb(
8485
'importMany',
85-
`cache ${cache}, throwForDependencyNotFound: ${throwForDependencyNotFound}. ids: {ids}`,
86+
`cache ${cache}, throwForDependencyNotFound: ${throwForDependencyNotFound}. ids: {ids}, lanes: {lanes}`,
8687
{
8788
ids: ids.toString(),
89+
lanes: lanes ? lanes.map((lane) => lane.id()).join(', ') : undefined,
8890
}
8991
);
9092
const idsToImport = compact(ids.filter((id) => id.hasScope()));
@@ -232,7 +234,7 @@ export default class ScopeComponentsImporter {
232234
* delta between the local head and the remote head. mainly to improve performance
233235
* not applicable and won't work for legacy. for legacy, refer to importManyWithAllVersions
234236
*/
235-
async importManyDeltaWithoutDeps(ids: BitIds, allHistory = false): Promise<void> {
237+
async importManyDeltaWithoutDeps(ids: BitIds, allHistory = false, lane?: Lane): Promise<void> {
236238
logger.debugAndAddBreadCrumb('importManyDeltaWithoutDeps', `Ids: {ids}`, { ids: ids.toString() });
237239
const idsWithoutNils = BitIds.uniqFromArray(compact(ids));
238240
if (R.isEmpty(idsWithoutNils)) return;
@@ -243,8 +245,7 @@ export default class ScopeComponentsImporter {
243245
// remove the version to fetch it with all versions.
244246
return id.changeVersion(undefined);
245247
}
246-
// @todo: fix to consider local lane
247-
const remoteLaneId = LaneId.from(DEFAULT_LANE, id.scope as string);
248+
const remoteLaneId = lane ? lane.toLaneId() : LaneId.from(DEFAULT_LANE, id.scope as string);
248249
const remoteHead = await this.repo.remoteLanes.getRef(remoteLaneId, id);
249250
if (!remoteHead) {
250251
return id.changeVersion(undefined);
@@ -258,7 +259,7 @@ export default class ScopeComponentsImporter {
258259
}
259260
return id.changeVersion(remoteHead.toString());
260261
});
261-
const groupedIds = groupByScopeName(idsToFetch);
262+
const groupedIds = lane ? groupByLanes(idsToFetch, [lane]) : groupByScopeName(idsToFetch);
262263
const idsOnlyDelta = idsToFetch.filter((id) => id.hasVersion());
263264
const idsAllHistory = idsToFetch.filter((id) => !id.hasVersion());
264265
const remotesCount = Object.keys(groupedIds).length;
@@ -273,8 +274,10 @@ export default class ScopeComponentsImporter {
273274
{
274275
type: 'component-delta',
275276
withoutDependencies: true,
277+
laneId: lane ? lane.id() : undefined,
276278
},
277-
idsToFetch
279+
idsToFetch,
280+
lane ? [lane] : undefined
278281
).fetchFromRemoteAndWrite();
279282
}
280283

@@ -338,15 +341,15 @@ export default class ScopeComponentsImporter {
338341
return compact(componentVersionArr);
339342
}
340343

341-
async fetchWithDeps(ids: BitIds, allowExternal: boolean): Promise<VersionDependencies[]> {
344+
async fetchWithDeps(ids: BitIds, allowExternal: boolean, onlyIfBuild = false): Promise<VersionDependencies[]> {
342345
logger.debugAndAddBreadCrumb('fetchWithDeps', `ids: {ids}`, { ids: ids.toString() });
343346
if (!allowExternal) this.throwIfExternalFound(ids);
344347
// avoid race condition of getting multiple "fetch" requests, which later translates into
345348
// multiple getExternalMany calls, which saves objects and write refs files at the same time
346349
return this.fetchWithDepsMutex.runExclusive(async () => {
347350
logger.debug('fetchWithDeps, acquiring a lock');
348351
const localDefs: ComponentDef[] = await this.sources.getMany(ids);
349-
const versionDeps = await this.multipleCompsDefsToVersionDeps(localDefs);
352+
const versionDeps = await this.multipleCompsDefsToVersionDeps(localDefs, undefined, onlyIfBuild);
350353
logger.debug('fetchWithDeps, releasing the lock');
351354
return versionDeps;
352355
});
@@ -453,7 +456,8 @@ export default class ScopeComponentsImporter {
453456

454457
private async multipleCompsDefsToVersionDeps(
455458
compsDefs: ComponentDef[],
456-
lanes: Lane[] = []
459+
lanes: Lane[] = [],
460+
onlyIfBuilt = false
457461
): Promise<VersionDependencies[]> {
458462
const concurrency = concurrentComponentsLimit();
459463
const componentsWithVersionsWithNulls = await pMap(
@@ -474,6 +478,14 @@ export default class ScopeComponentsImporter {
474478
if (!version) {
475479
throw new Error(`ScopeComponentImporter, expect ${id.toString()} to have a Version object`);
476480
}
481+
if (onlyIfBuilt && version.buildStatus !== BuildStatus.Succeed) {
482+
logger.debug(
483+
`multipleCompsDefsToVersionDeps, id: ${id.toString()} is skipped because its build-status is ${
484+
version.buildStatus
485+
}`
486+
);
487+
return null;
488+
}
477489

478490
return { componentVersion: versionComp, versionObj: version };
479491
},
@@ -508,21 +520,29 @@ export default class ScopeComponentsImporter {
508520
lanes: Lane[] = []
509521
): Promise<VersionDependencies[]> {
510522
if (!ids.length) return [];
511-
logger.debugAndAddBreadCrumb('ScopeComponentsImporter.getExternalMany', `fetching from remote scope. Ids: {ids}`, {
512-
ids: ids.join(', '),
513-
});
523+
logger.debugAndAddBreadCrumb(
524+
'ScopeComponentsImporter.getExternalMany',
525+
`fetching from remote scope. Ids: {ids}, Lanes: {lanes}`,
526+
{
527+
ids: ids.join(', '),
528+
lanes: lanes.map((lane) => lane.id()).join(', '),
529+
}
530+
);
514531
const context = {};
515532
ids.forEach((id) => {
516533
if (id.isLocal(this.scope.name))
517534
throw new Error(`getExternalMany expects to get external ids only, got ${id.toString()}`);
518535
});
519536
enrichContextFromGlobal(Object.assign({}, { requestedBitIds: ids.map((id) => id.toString()) }));
537+
// avoid re-fetching the components with all deps if they're still un-built
538+
const onlyIfBuilt = ids.every((id) => this.sources.isUnBuiltInCache(id));
520539
await new ObjectFetcher(
521540
this.repo,
522541
this.scope,
523542
remotes,
524543
{
525544
withoutDependencies: false,
545+
onlyIfBuilt,
526546
},
527547
ids,
528548
lanes,

src/scope/models/lane.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { v4 } from 'uuid';
22
import { isHash } from '@teambit/component-version';
33
import { LaneId, DEFAULT_LANE, LANE_REMOTE_DELIMITER } from '@teambit/lane-id';
44
import { Scope } from '..';
5-
import { BitId } from '../../bit-id';
5+
import { BitId, BitIds } from '../../bit-id';
66
import { CFG_USER_EMAIL_KEY, CFG_USER_NAME_KEY, PREVIOUS_DEFAULT_LANE } from '../../constants';
77
import ValidationError from '../../error/validation-error';
88
import logger from '../../logger/logger';
@@ -181,8 +181,8 @@ export default class Lane extends BitObject {
181181
);
182182
return { merged, unmerged };
183183
}
184-
toBitIds(): BitId[] {
185-
return this.components.map((c) => c.id.changeVersion(c.head.toString()));
184+
toBitIds(): BitIds {
185+
return BitIds.fromArray(this.components.map((c) => c.id.changeVersion(c.head.toString())));
186186
}
187187
toLaneId() {
188188
return new LaneId({ scope: this.scope, name: this.name });

src/scope/repositories/sources.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export type MergeResult = {
4949
mergedVersions: string[];
5050
};
5151

52-
const MAX_AGE_UN_BUILT_COMPS_CACHE = 60 * 1000;
52+
const MAX_AGE_UN_BUILT_COMPS_CACHE = 60 * 1000; // 1 min
5353

5454
export default class SourceRepository {
5555
scope: Scope;
@@ -155,6 +155,10 @@ export default class SourceRepository {
155155
return returnComponent(version as Version);
156156
}
157157

158+
isUnBuiltInCache(bitId: BitId): boolean {
159+
return Boolean(this.cacheUnBuiltIds.get(bitId.toString()));
160+
}
161+
158162
async _findComponent(component: ModelComponent): Promise<ModelComponent | undefined> {
159163
try {
160164
const foundComponent = await this.objects().load(component.hash());

0 commit comments

Comments
 (0)