@teambit/workspace 1.0.1017 → 1.0.1019

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,13 @@ import { EnvsAspect } from '@teambit/envs';
21
21
  import { ExtensionDataEntry, ExtensionDataList } from '@teambit/legacy.extension-data';
22
22
  import type { InMemoryCache } from '@teambit/harmony.modules.in-memory-cache';
23
23
  import { getMaxSizeForComponents, createInMemoryCache } from '@teambit/harmony.modules.in-memory-cache';
24
+ import type { LoadSpan } from '@teambit/harmony.modules.load-trace';
25
+ import {
26
+ startOrJoinLoadTrace,
27
+ loadSpan,
28
+ currentLoadTrace,
29
+ reportLoadFailure,
30
+ } from '@teambit/harmony.modules.load-trace';
24
31
  import type { AspectLoaderMain } from '@teambit/aspect-loader';
25
32
  import type { Workspace } from '../workspace';
26
33
  import { WorkspaceComponent } from './workspace-component';
@@ -121,9 +128,20 @@ export class WorkspaceComponentLoader {
121
128
  if (!idsWithoutEmpty.length) {
122
129
  return { components: [], invalidComponents: [] };
123
130
  }
131
+ return startOrJoinLoadTrace('workspace.getMany', { ids: idsWithoutEmpty.length }, (span) =>
132
+ this.getManyWithSpan(idsWithoutEmpty, span, loadOpts, throwOnFailure)
133
+ );
134
+ }
135
+
136
+ private async getManyWithSpan(
137
+ idsWithoutEmpty: Array<ComponentID>,
138
+ span: LoadSpan,
139
+ loadOpts?: ComponentLoadOptions,
140
+ throwOnFailure = true
141
+ ): Promise<GetManyRes> {
124
142
  const callId = Math.floor(Math.random() * 1000); // generate a random callId to be able to identify the call from the logs
125
143
  this.logger.profileTrace(`getMany-${callId}`);
126
- this.logger.setStatusLine(`loading ${ids.length} component(s)`);
144
+ this.logger.setStatusLine(`loading ${idsWithoutEmpty.length} component(s)`);
127
145
  const loadOptsWithDefaults: ComponentLoadOptions = Object.assign(
128
146
  // We don't want to load extension or execute the load slot at this step
129
147
  // we will do it later
@@ -145,6 +163,8 @@ export class WorkspaceComponentLoader {
145
163
  }
146
164
  }, loadOrCached);
147
165
 
166
+ span.setAttribute('componentsCache:hits', loadOrCached.fromCache.length);
167
+ span.setAttribute('componentsCache:misses', loadOrCached.idsToLoad.length);
148
168
  const { components: loadedComponents, invalidComponents } = await this.getAndLoadSlotOrdered(
149
169
  loadOrCached.idsToLoad || [],
150
170
  loadOptsWithDefaults,
@@ -163,7 +183,7 @@ export class WorkspaceComponentLoader {
163
183
  components.forEach((comp) => {
164
184
  this.saveInCache(comp, { loadExtensions: true, executeLoadSlot: true });
165
185
  });
166
- const idsWithEmptyStrs = ids.map((id) => id.toString());
186
+ const idsWithEmptyStrs = idsWithoutEmpty.map((id) => id.toString());
167
187
  const requestedComponents = components.filter(
168
188
  (comp) =>
169
189
  idsWithEmptyStrs.includes(comp.id.toString()) || idsWithEmptyStrs.includes(comp.id.toStringWithoutVersion())
@@ -180,9 +200,11 @@ export class WorkspaceComponentLoader {
180
200
  ): Promise<GetManyRes> {
181
201
  if (!ids?.length) return { components: [], invalidComponents: [] };
182
202
 
183
- const workspaceScopeIdsMap: WorkspaceScopeIdsMap = await this.groupAndUpdateIds(ids);
203
+ const workspaceScopeIdsMap: WorkspaceScopeIdsMap = await loadSpan('id-resolution', { ids: ids.length }, () =>
204
+ this.groupAndUpdateIds(ids)
205
+ );
184
206
  this.logger.profileTrace('buildLoadGroups');
185
- const groupsToHandle = await this.buildLoadGroups(workspaceScopeIdsMap);
207
+ const groupsToHandle = await loadSpan('build-load-groups', {}, () => this.buildLoadGroups(workspaceScopeIdsMap));
186
208
  this.logger.profileTrace('buildLoadGroups');
187
209
  // prefix your command with "BIT_LOG=*" to see the detailed groups
188
210
  if (process.env.BIT_LOG) {
@@ -191,12 +213,17 @@ export class WorkspaceComponentLoader {
191
213
  const groupsRes = compact(
192
214
  await mapSeries(groupsToHandle, async (group, index) => {
193
215
  const { scopeIds, workspaceIds, aspects, core, seeders, envs } = group;
194
- const groupDesc = `getMany-${callId} group ${index + 1}/${groupsToHandle.length} - ${loadGroupToStr(group)}`;
216
+ const groupStr = loadGroupToStr(group);
217
+ const groupDesc = `getMany-${callId} group ${index + 1}/${groupsToHandle.length} - ${groupStr}`;
195
218
  this.logger.profileTrace(groupDesc);
196
219
  if (!workspaceIds.length && !scopeIds.length) {
197
220
  throw new Error('getAndLoadSlotOrdered - group has no ids to load');
198
221
  }
199
- const res = await this.getAndLoadSlot(workspaceIds, scopeIds, { ...loadOpts, core, seeders, aspects, envs });
222
+ const res = await loadSpan(
223
+ 'load-group',
224
+ { group: `${index + 1}/${groupsToHandle.length}`, desc: groupStr },
225
+ () => this.getAndLoadSlot(workspaceIds, scopeIds, { ...loadOpts, core, seeders, aspects, envs })
226
+ );
200
227
  this.logger.profileTrace(groupDesc);
201
228
  // We don't want to return components that were not asked originally (we do want to load them)
202
229
  if (!group.seeders) return undefined;
@@ -413,7 +440,9 @@ export class WorkspaceComponentLoader {
413
440
  });
414
441
  if (loadOpts.loadExtensions) {
415
442
  this.logger.profileTrace('loadComponentsExtensions');
416
- await this.workspace.loadComponentsExtensions(filteredMergeExtensions);
443
+ await loadSpan('load-components-extensions', { extensions: filteredMergeExtensions.length }, () =>
444
+ this.workspace.loadComponentsExtensions(filteredMergeExtensions)
445
+ );
417
446
  this.logger.profileTrace('loadComponentsExtensions');
418
447
  }
419
448
  let wsComponentsWithAspects = workspaceComponents;
@@ -433,17 +462,21 @@ export class WorkspaceComponentLoader {
433
462
  // so we will get wrong dependencies from component who uses envs from the workspace
434
463
  this.logger.profileTrace('loadCompsAsAspects');
435
464
  if (loadOpts.loadSeedersAsAspects || (loadOpts.core && loadOpts.aspects)) {
436
- await this.loadCompsAsAspects(workspaceComponents.concat(scopeComponents), {
437
- loadApps: true,
438
- loadEnvs: true,
439
- loadAspects: loadOpts.aspects,
440
- core: loadOpts.core,
441
- seeders: loadOpts.seeders,
442
- idsToNotLoadAsAspects: loadOpts.idsToNotLoadAsAspects,
443
- });
465
+ await loadSpan('load-comps-as-aspects', { components: workspaceComponents.length + scopeComponents.length }, () =>
466
+ this.loadCompsAsAspects(workspaceComponents.concat(scopeComponents), {
467
+ loadApps: true,
468
+ loadEnvs: true,
469
+ loadAspects: loadOpts.aspects,
470
+ core: loadOpts.core,
471
+ seeders: loadOpts.seeders,
472
+ idsToNotLoadAsAspects: loadOpts.idsToNotLoadAsAspects,
473
+ })
474
+ );
444
475
  }
445
476
  this.logger.profileTrace('loadCompsAsAspects');
446
477
 
478
+ this.attachReportedLoadFailures(withAspects);
479
+
447
480
  return { components: withAspects, invalidComponents };
448
481
  }
449
482
 
@@ -485,14 +518,81 @@ export class WorkspaceComponentLoader {
485
518
  await this.workspace.loadAspects(aspectIds, true, 'self loading aspects', { useScopeAspectsCapsule: true });
486
519
  } catch (err: any) {
487
520
  this.logger.warn(`failed loading components as aspects for components ${aspectIds.join(', ')}`, err);
488
- // we ignore that errors at the moment
521
+ // the error is not thrown (best-effort loading), but it's surfaced as a component issue
522
+ // so it shows up in "bit status" instead of disappearing.
523
+ if (!this.workspace.inInstallContext) {
524
+ const errMsg = err.message || String(err);
525
+ aspectIds.forEach((aspectId) => {
526
+ reportLoadFailure({ failedId: aspectId, phase: 'load-comps-as-aspects', error: errMsg });
527
+ });
528
+ components.forEach((component) => {
529
+ if (!aspectIds.includes(component.id.toString())) return;
530
+ this.addLoadFailureIssue(component, component.id.toString(), 'load-comps-as-aspects', errMsg);
531
+ });
532
+ }
489
533
  }
490
534
  }
491
535
 
536
+ private addLoadFailureIssue(component: Component, failedId: string, phase: string, error: string) {
537
+ const consumerComponent = component.state._consumer;
538
+ if (!consumerComponent) return;
539
+ const issue = component.state.issues.getOrCreate(IssuesClasses.LoadFailures);
540
+ const alreadyReported = issue.data.some((entry) => entry.failedId === failedId && entry.phase === phase);
541
+ if (!alreadyReported) issue.data.push({ failedId, phase, error });
542
+ }
543
+
544
+ /**
545
+ * surface load failures that were reported to the active trace by deep loader code (e.g. aspect
546
+ * require failures in the scope loader). the failing component itself gets a per-component
547
+ * issue. components merely *using* the failed aspect/env are aggregated into a single
548
+ * workspace-level issue — one failing env may be used by 100 components, and attaching the
549
+ * issue to each of them would flood "bit status".
550
+ */
551
+ private attachReportedLoadFailures(components: Component[]) {
552
+ if (this.workspace.inInstallContext) return;
553
+ const failures = currentLoadTrace()?.loadFailures;
554
+ if (!failures?.length) return;
555
+ const noVersion = (id: string) => id.split('@')[0];
556
+ components.forEach((component) => {
557
+ const componentIdStr = component.id.toString();
558
+ const usedIds: string[] = component.state._consumer
559
+ ? component.state._consumer.extensions.map((ext) => ext.stringId)
560
+ : [];
561
+ // the env may be set via the EnvsAspect config rather than as a direct extension entry, in
562
+ // which case its id won't appear in the extensions list above. include it explicitly so a
563
+ // failure loading that env still gets aggregated to this component.
564
+ const envId = component.state.aspects.get(EnvsAspect.id)?.data?.id;
565
+ if (envId) usedIds.push(envId);
566
+ failures.forEach((failure) => {
567
+ if (noVersion(failure.failedId) === noVersion(componentIdStr)) {
568
+ this.addLoadFailureIssue(component, failure.failedId, failure.phase, failure.error);
569
+ return;
570
+ }
571
+ if (usedIds.some((usedId) => noVersion(usedId) === noVersion(failure.failedId))) {
572
+ this.workspace.registerAggregatedLoadFailure(failure, componentIdStr);
573
+ }
574
+ });
575
+ });
576
+ }
577
+
492
578
  private async populateScopeAndExtensionsCache(ids: ComponentID[], workspaceScopeIdsMap: WorkspaceScopeIdsMap) {
493
- return mapSeries(ids, async (id) => {
579
+ return loadSpan('populate-scope-and-extensions-cache', { ids: ids.length }, (span) =>
580
+ this.populateScopeAndExtensionsCacheWithSpan(ids, workspaceScopeIdsMap, span)
581
+ );
582
+ }
583
+
584
+ private async populateScopeAndExtensionsCacheWithSpan(
585
+ ids: ComponentID[],
586
+ workspaceScopeIdsMap: WorkspaceScopeIdsMap,
587
+ span: LoadSpan
588
+ ) {
589
+ let scopeCacheHits = 0;
590
+ let extensionsCacheHits = 0;
591
+ const result = await mapSeries(ids, async (id) => {
494
592
  const idStr = id.toString();
495
593
  let componentFromScope;
594
+ if (this.scopeComponentsCache.has(idStr)) scopeCacheHits += 1;
595
+ if (this.componentsExtensionsCache.has(idStr)) extensionsCacheHits += 1;
496
596
  if (!this.scopeComponentsCache.has(idStr)) {
497
597
  try {
498
598
  // Do not import automatically if it's missing, it will throw an error later
@@ -520,6 +620,10 @@ export class WorkspaceComponentLoader {
520
620
  this.componentsExtensionsCache.set(idStr, { extensions, errors, envId });
521
621
  }
522
622
  });
623
+ span.setAttribute('scopeComponentsCache:hits', scopeCacheHits);
624
+ span.setAttribute('scopeComponentsCache:misses', ids.length - scopeCacheHits);
625
+ span.setAttribute('componentsExtensionsCache:hits', extensionsCacheHits);
626
+ return result;
523
627
  }
524
628
 
525
629
  private async warnAboutMisconfiguredEnvs(components: Component[]) {
@@ -587,10 +691,8 @@ export class WorkspaceComponentLoader {
587
691
  components: legacyComponents,
588
692
  invalidComponents: legacyInvalidComponents,
589
693
  removedComponents,
590
- } = await this.workspace.consumer.loadComponents(
591
- ComponentIdList.fromArray(workspaceIds),
592
- false,
593
- loadOptsWithDefaults
694
+ } = await loadSpan('consumer-fs-load', { ids: workspaceIds.length }, () =>
695
+ this.workspace.consumer.loadComponents(ComponentIdList.fromArray(workspaceIds), false, loadOptsWithDefaults)
594
696
  );
595
697
  this.logger.profileTrace('consumer.loadComponents');
596
698
  const allLegacyComponents = legacyComponents.concat(removedComponents);
@@ -659,7 +761,9 @@ export class WorkspaceComponentLoader {
659
761
  // as when loading the next batch of components (next group) we won't have the envs loaded
660
762
 
661
763
  try {
662
- const scopeComponents = await this.workspace.scope.getMany(scopeIds);
764
+ const scopeComponents = await loadSpan('scope-load', { ids: scopeIds.length }, () =>
765
+ this.workspace.scope.getMany(scopeIds)
766
+ );
663
767
 
664
768
  // We don't want to load envs as part of this step, they will be loaded later
665
769
  // const scopeComponents = await this.workspace.scope.loadMany(scopeIds, undefined, {
@@ -708,6 +812,20 @@ export class WorkspaceComponentLoader {
708
812
  storeInCache = true,
709
813
  loadOpts?: ComponentLoadOptions,
710
814
  getOpts: ComponentGetOneOptions = { resolveIdVersion: true }
815
+ ): Promise<Component> {
816
+ return startOrJoinLoadTrace('workspace.get', { id: componentId.toString() }, (span) =>
817
+ this.getWithSpan(componentId, span, legacyComponent, useCache, storeInCache, loadOpts, getOpts)
818
+ );
819
+ }
820
+
821
+ private async getWithSpan(
822
+ componentId: ComponentID,
823
+ span: LoadSpan,
824
+ legacyComponent?: ConsumerComponent,
825
+ useCache = true,
826
+ storeInCache = true,
827
+ loadOpts?: ComponentLoadOptions,
828
+ getOpts: ComponentGetOneOptions = { resolveIdVersion: true }
711
829
  ): Promise<Component> {
712
830
  const loadOptsWithDefaults: ComponentLoadOptions = Object.assign(
713
831
  { loadExtensions: true, executeLoadSlot: true },
@@ -716,17 +834,22 @@ export class WorkspaceComponentLoader {
716
834
  const id = getOpts?.resolveIdVersion ? this.resolveVersion(componentId) : componentId;
717
835
  const fromCache = this.getFromCache(id, loadOptsWithDefaults);
718
836
  if (fromCache && useCache) {
837
+ span.setAttribute('componentsCache', 'hit');
719
838
  return fromCache;
720
839
  }
840
+ span.setAttribute('componentsCache', 'miss');
721
841
  let consumerComponent = legacyComponent;
722
842
  const inWs = await this.isInWsIncludeDeleted(id);
723
843
  if (inWs && !consumerComponent) {
724
- consumerComponent = await this.getConsumerComponent(id, loadOptsWithDefaults);
844
+ consumerComponent = await loadSpan('consumer-fs-load', { id: id.toString() }, () =>
845
+ this.getConsumerComponent(id, loadOptsWithDefaults)
846
+ );
725
847
  }
726
848
 
727
849
  // in case of out-of-sync, the id may changed during the load process
728
850
  const updatedId = consumerComponent ? consumerComponent.id : id;
729
851
  const component = await this.loadOne(updatedId, consumerComponent, loadOptsWithDefaults);
852
+ this.attachReportedLoadFailures([component]);
730
853
  if (storeInCache) {
731
854
  this.addMultipleEnvsIssueIfNeeded(component); // it's in storeInCache block, otherwise, it wasn't fully loaded
732
855
  this.saveInCache(component, loadOptsWithDefaults);
@@ -789,9 +912,10 @@ export class WorkspaceComponentLoader {
789
912
 
790
913
  private async loadOne(id: ComponentID, consumerComponent?: ConsumerComponent, loadOpts?: ComponentLoadOptions) {
791
914
  const idStr = id.toString();
792
- const componentFromScope = this.scopeComponentsCache.has(idStr)
915
+ const scopeCacheHit = this.scopeComponentsCache.has(idStr);
916
+ const componentFromScope = scopeCacheHit
793
917
  ? this.scopeComponentsCache.get(idStr)
794
- : await this.workspace.scope.get(id);
918
+ : await loadSpan('scope-load', { id: idStr }, () => this.workspace.scope.get(id));
795
919
  if (!consumerComponent) {
796
920
  if (!componentFromScope) throw new MissingBitMapComponent(id.toString());
797
921
  return componentFromScope;
@@ -801,9 +925,14 @@ export class WorkspaceComponentLoader {
801
925
  : undefined;
802
926
  const { extensions, errors } =
803
927
  extErrorsFromCache ||
804
- (await this.workspace.componentExtensions(id, componentFromScope, undefined, {
805
- loadExtensions: loadOpts?.loadExtensions,
806
- }));
928
+ (await loadSpan(
929
+ 'extension-merge',
930
+ { id: idStr, scopeComponentsCache: scopeCacheHit ? 'hit' : 'miss', componentsExtensionsCache: 'miss' },
931
+ () =>
932
+ this.workspace.componentExtensions(id, componentFromScope, undefined, {
933
+ loadExtensions: loadOpts?.loadExtensions,
934
+ })
935
+ ));
807
936
  if (errors?.some((err) => err instanceof MergeConfigConflict)) {
808
937
  consumerComponent.issues.getOrCreate(IssuesClasses.MergeConfigHasConflict).data = true;
809
938
  }
@@ -902,6 +1031,12 @@ export class WorkspaceComponentLoader {
902
1031
  }
903
1032
 
904
1033
  private async executeLoadSlot(component: Component, loadOpts?: ComponentLoadOptions) {
1034
+ return loadSpan('execute-load-slot', { id: component.id.toString() }, () =>
1035
+ this.executeLoadSlotWithSpan(component, loadOpts)
1036
+ );
1037
+ }
1038
+
1039
+ private async executeLoadSlotWithSpan(component: Component, loadOpts?: ComponentLoadOptions) {
905
1040
  if (component.state._consumer.removed) {
906
1041
  // if it was soft-removed now, the component is not in the FS. loading aspects such as composition ends up with
907
1042
  // errors as they try to read component files from the filesystem.
@@ -910,7 +1045,9 @@ export class WorkspaceComponentLoader {
910
1045
 
911
1046
  // Special load events which runs from the workspace but should run from the correct aspect
912
1047
  // TODO: remove this once those extensions dependent on workspace
913
- const envsData = await this.envs.calcDescriptor(component, { skipWarnings: !!this.workspace.inInstallContext });
1048
+ const envsData = await loadSpan('env-calc', {}, () =>
1049
+ this.envs.calcDescriptor(component, { skipWarnings: !!this.workspace.inInstallContext })
1050
+ );
914
1051
 
915
1052
  const wsDeps = component.state._consumer.dependencies.dependencies || [];
916
1053
  const modelDeps = component.state._consumer.componentFromModel?.dependencies.dependencies || [];
@@ -918,17 +1055,20 @@ export class WorkspaceComponentLoader {
918
1055
  const envExtendsDeps = merged.get();
919
1056
 
920
1057
  // Move to deps resolver main runtime once we switch ws<> deps resolver direction
921
- const policy = await this.dependencyResolver.mergeVariantPolicies(
922
- component.config.extensions,
923
- component.id,
924
- component.state._consumer.files,
925
- envExtendsDeps
926
- );
927
- const dependenciesList = await this.dependencyResolver.extractDepsFromLegacy(component, policy);
928
- const resolvedEnvJsonc = await this.envs.calculateEnvManifest(
929
- component,
930
- component.state._consumer.files,
931
- envExtendsDeps
1058
+ const { policy, dependenciesList } = await loadSpan('dependency-resolution', {}, async () => {
1059
+ const mergedPolicy = await this.dependencyResolver.mergeVariantPolicies(
1060
+ component.config.extensions,
1061
+ component.id,
1062
+ component.state._consumer.files,
1063
+ envExtendsDeps
1064
+ );
1065
+ return {
1066
+ policy: mergedPolicy,
1067
+ dependenciesList: await this.dependencyResolver.extractDepsFromLegacy(component, mergedPolicy),
1068
+ };
1069
+ });
1070
+ const resolvedEnvJsonc = await loadSpan('env-manifest', {}, () =>
1071
+ this.envs.calculateEnvManifest(component, component.state._consumer.files, envExtendsDeps)
932
1072
  );
933
1073
  if (resolvedEnvJsonc) {
934
1074
  // @ts-ignore
@@ -957,7 +1097,7 @@ export class WorkspaceComponentLoader {
957
1097
 
958
1098
  const entries = this.workspace.onComponentLoadSlot.toArray();
959
1099
  await mapSeries(entries, async ([extension, onLoad]) => {
960
- const data = await onLoad(component, loadOpts);
1100
+ const data = await loadSpan('on-load', { aspect: extension }, () => onLoad(component, loadOpts));
961
1101
  await this.upsertExtensionData(component, extension, data);
962
1102
  // Update the aspect list to have changes happened during the on load slot (new data added above)
963
1103
  component.state.aspects.upsertEntry(await this.workspace.resolveComponentId(extension), data);