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

Development: Improve performance of programming exercise details view #9785

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2a3801f
Prevent unnecessary re-rendering of details page
florian-glombik Nov 14, 2024
e7b7bbe
Get rid of duplicated code
florian-glombik Nov 14, 2024
96751b1
Refactor duplicated code for line calculation
florian-glombik Nov 14, 2024
34a8a74
Introduce signals in detail overview list component
florian-glombik Nov 14, 2024
4f9d814
Remove unused variable
florian-glombik Nov 14, 2024
ffbb6b6
Address Coderabbit change requests
florian-glombik Nov 15, 2024
fd45565
Fix client test
florian-glombik Nov 15, 2024
78679a2
Solve without using switchMap
florian-glombik Nov 15, 2024
fa3c8b2
SwitchMap is required for loading the diff
florian-glombik Nov 15, 2024
6a7339e
Fix client tests
florian-glombik Nov 15, 2024
a798143
Fix client tests
florian-glombik Nov 15, 2024
0f2ad0e
Fix client tests
florian-glombik Nov 15, 2024
cd48478
Make sure to unsubscribe on dismount
florian-glombik Nov 15, 2024
fba925e
Merge branch 'develop' into chore/programming-exercises/reduce-api-ca…
florian-glombik Nov 15, 2024
857cf7b
Switch approach back to use switchMap
florian-glombik Nov 16, 2024
bf1e97f
Fix tests
florian-glombik Nov 16, 2024
2eca88b
Add tests for regex utils
florian-glombik Nov 16, 2024
e64dd14
Add tests for keyboard utils
florian-glombik Nov 16, 2024
06d0d25
Add tests for exif utils
florian-glombik Nov 16, 2024
6253315
Re-add and fix client tests
florian-glombik Nov 18, 2024
79fd9cd
Fix existing tests by adding more mocks
florian-glombik Nov 20, 2024
bb02856
Remove further client tests from diff
florian-glombik Nov 20, 2024
c5bbcb8
Merge branch 'develop' into chore/programming-exercises/reduce-api-ca…
florian-glombik Nov 20, 2024
5fea36e
Move gitDiff subscription back to mergeMap
florian-glombik Nov 20, 2024
2947647
Use switchMap instead of concatMap
florian-glombik Nov 20, 2024
0b932e5
Fix loading of build logs
florian-glombik Nov 20, 2024
938f2bc
Fix retrieving build logs
florian-glombik Nov 20, 2024
b933c15
Use mergeMap instead of switchMap
florian-glombik Nov 20, 2024
48233d7
Merge branch 'develop' into chore/programming-exercises/reduce-api-ca…
florian-glombik Nov 21, 2024
964833d
Merge branch 'develop' into chore/programming-exercises/reduce-api-ca…
florian-glombik Nov 22, 2024
f8eaad8
Fix request loop on triggering build
florian-glombik Nov 23, 2024
810b717
Remove async from tap
florian-glombik Nov 23, 2024
8d3f559
Merge branch 'develop' into chore/programming-exercises/reduce-api-ca…
florian-glombik Nov 24, 2024
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
@@ -1,7 +1,7 @@
@if (headlines?.length && headlines.length > 1) {
<jhi-detail-overview-navigation-bar [sectionHeadlines]="headlines" />
}
@for (section of sections; track section) {
@for (section of sections(); track section) {
<h3 class="section-headline" [id]="headlinesRecord[section.headline]">{{ section.headline | artemisTranslate }}</h3>
<dl class="section-detail-list">
@for (detail of section.details; track $index) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation, inject, input } from '@angular/core';
import { isEmpty } from 'lodash-es';
import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service';
import { ButtonSize, TooltipPlacement } from 'app/shared/components/button.component';
import { ButtonSize } from 'app/shared/components/button.component';
import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model';
import { ModelingExerciseService } from 'app/exercises/modeling/manage/modeling-exercise.service';
import { AlertService } from 'app/core/util/alert.service';
Expand Down Expand Up @@ -50,11 +50,13 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy {
protected readonly FeatureToggle = FeatureToggle;
protected readonly ButtonSize = ButtonSize;
protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType;
protected readonly CHAT = IrisSubSettingsType.CHAT;

readonly CHAT = IrisSubSettingsType.CHAT;
private readonly modelingExerciseService = inject(ModelingExerciseService);
private readonly alertService = inject(AlertService);
private readonly profileService = inject(ProfileService);

@Input()
sections: DetailOverviewSection[];
sections = input.required<DetailOverviewSection[]>();

// headline list for navigation bar
headlines: { id: string; translationKey: string }[];
Expand All @@ -64,14 +66,8 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy {
profileSubscription: Subscription;
isLocalVC = false;

constructor(
private modelingExerciseService: ModelingExerciseService,
private alertService: AlertService,
private profileService: ProfileService,
) {}

ngOnInit() {
this.headlines = this.sections.map((section) => {
this.headlines = this.sections().map((section) => {
return {
id: section.headline.replaceAll('.', '-'),
translationKey: section.headline,
Expand All @@ -98,6 +94,4 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.profileSubscription?.unsubscribe();
}

protected readonly TooltipPlacement = TooltipPlacement;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SafeHtml } from '@angular/platform-browser';
import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config';
import { Subject, Subscription } from 'rxjs';
import { Subject, Subscription, of } from 'rxjs';
import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model';
import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service';
import { AlertService, AlertType } from 'app/core/util/alert.service';
Expand Down Expand Up @@ -57,6 +57,9 @@ import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-setting
import { Detail } from 'app/detail-overview-list/detail.model';
import { Competency } from 'app/entities/competency.model';
import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service';
import { mergeMap, tap } from 'rxjs/operators';
import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model';
import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto';

@Component({
selector: 'jhi-programming-exercise-detail',
Expand All @@ -65,15 +68,32 @@ import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.s
encapsulation: ViewEncapsulation.None,
})
export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
readonly dayjs = dayjs;
readonly ActionType = ActionType;
readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType;
readonly FeatureToggle = FeatureToggle;
readonly ProgrammingLanguage = ProgrammingLanguage;
readonly PROGRAMMING = ExerciseType.PROGRAMMING;
readonly ButtonSize = ButtonSize;
readonly AssessmentType = AssessmentType;
readonly documentationType: DocumentationType = 'Programming';
protected readonly dayjs = dayjs;
protected readonly ActionType = ActionType;
protected readonly ProgrammingExerciseParticipationType = ProgrammingExerciseParticipationType;
protected readonly FeatureToggle = FeatureToggle;
protected readonly ProgrammingLanguage = ProgrammingLanguage;
protected readonly PROGRAMMING = ExerciseType.PROGRAMMING;
protected readonly ButtonSize = ButtonSize;
protected readonly AssessmentType = AssessmentType;
protected readonly documentationType: DocumentationType = 'Programming';

protected readonly faUndo = faUndo;
protected readonly faTrash = faTrash;
protected readonly faBook = faBook;
protected readonly faWrench = faWrench;
protected readonly faCheckDouble = faCheckDouble;
protected readonly faTable = faTable;
protected readonly faExclamationTriangle = faExclamationTriangle;
protected readonly faFileSignature = faFileSignature;
protected readonly faListAlt = faListAlt;
protected readonly faChartBar = faChartBar;
protected readonly faLightbulb = faLightbulb;
protected readonly faPencilAlt = faPencilAlt;
protected readonly faUsers = faUsers;
protected readonly faEye = faEye;
protected readonly faUserCheck = faUserCheck;
protected readonly faRobot = faRobot;

programmingExercise: ProgrammingExercise;
programmingExerciseBuildConfig?: ProgrammingExerciseBuildConfig;
Expand Down Expand Up @@ -106,35 +126,14 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {

private activatedRouteSubscription: Subscription;
private templateAndSolutionParticipationSubscription: Subscription;
private profileInfoSubscription: Subscription;
private irisSettingsSubscription: Subscription;
private submissionPolicySubscription: Subscription;
private buildLogsSubscription: Subscription;
private exerciseStatisticsSubscription: Subscription;

private dialogErrorSource = new Subject<string>();
dialogError$ = this.dialogErrorSource.asObservable();

exerciseDetailSections: DetailOverviewSection[];

// Icons
faUndo = faUndo;
faTrash = faTrash;
faBook = faBook;
faWrench = faWrench;
faCheckDouble = faCheckDouble;
faTable = faTable;
faExclamationTriangle = faExclamationTriangle;
faFileSignature = faFileSignature;
faListAlt = faListAlt;
faChartBar = faChartBar;
faLightbulb = faLightbulb;
faPencilAlt = faPencilAlt;
faUsers = faUsers;
faEye = faEye;
faUserCheck = faUserCheck;
faRobot = faRobot;

constructor(
private activatedRoute: ActivatedRoute,
private accountService: AccountService,
Expand Down Expand Up @@ -184,13 +183,15 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {

this.templateAndSolutionParticipationSubscription = this.programmingExerciseService
.findWithTemplateAndSolutionParticipationAndLatestResults(programmingExercise.id!)
.subscribe((updatedProgrammingExercise) => {
this.programmingExercise = updatedProgrammingExercise.body!;

this.setLatestCoveredLineRatio();
this.loadingTemplateParticipationResults = false;
this.loadingSolutionParticipationResults = false;
this.profileInfoSubscription = this.profileService.getProfileInfo().subscribe(async (profileInfo) => {
.pipe(
tap((updatedProgrammingExercise) => {
this.programmingExercise = updatedProgrammingExercise.body!;
this.setLatestCoveredLineRatio();
this.loadingTemplateParticipationResults = false;
this.loadingSolutionParticipationResults = false;
}),
mergeMap(() => this.profileService.getProfileInfo()),
tap((profileInfo) => {
if (profileInfo) {
if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) {
this.programmingExercise.templateParticipation.buildPlanUrl = createBuildPlanUrl(
Expand All @@ -215,38 +216,41 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
if (this.irisEnabled) {
this.irisSettingsSubscription = this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => {
this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false;
this.exerciseDetailSections = this.getExerciseDetails();
});
}
}
}),
mergeMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId)),
tap((submissionPolicy) => {
this.programmingExercise.submissionPolicy = submissionPolicy;
}),
mergeMap(() => this.programmingExerciseService.getDiffReport(exerciseId)),
tap((gitDiffReport) => {
this.processGitDiffReport(gitDiffReport, false);
}),
mergeMap(() =>
this.programmingExercise.isAtLeastEditor ? this.programmingExerciseService.getBuildLogStatistics(exerciseId!) : of([] as BuildLogStatisticsDTO),
),
tap((buildLogStatistics) => {
if (this.programmingExercise.isAtLeastEditor) {
this.programmingExercise.buildLogStatistics = buildLogStatistics;
}
}),
)
.subscribe({
next: () => {
this.setLatestCoveredLineRatio();
this.checkAndAlertInconsistencies();
this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(
programmingExercise.programmingLanguage,
).plagiarismCheckSupported;

/** we make sure to await the results of the subscriptions (switchMap) to only call {@link getExerciseDetails} once */
this.exerciseDetailSections = this.getExerciseDetails();
});

this.submissionPolicySubscription = this.programmingExerciseSubmissionPolicyService
.getSubmissionPolicyOfProgrammingExercise(exerciseId!)
.subscribe((submissionPolicy) => {
this.programmingExercise.submissionPolicy = submissionPolicy;
this.exerciseDetailSections = this.getExerciseDetails();
});

this.loadGitDiffReport();

// the build logs endpoint requires at least editor privileges
if (this.programmingExercise.isAtLeastEditor) {
this.buildLogsSubscription = this.programmingExerciseService
.getBuildLogStatistics(exerciseId!)
.subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics));
this.exerciseDetailSections = this.getExerciseDetails();
}

this.setLatestCoveredLineRatio();

this.checkAndAlertInconsistencies();

this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(
programmingExercise.programmingLanguage,
).plagiarismCheckSupported;
this.exerciseDetailSections = this.getExerciseDetails();
},
error: (error) => {
this.alertService.error(error.message);
},
florian-glombik marked this conversation as resolved.
Show resolved Hide resolved
});

this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => {
Expand All @@ -259,13 +263,17 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
this.dialogErrorSource.unsubscribe();
this.activatedRouteSubscription?.unsubscribe();
this.templateAndSolutionParticipationSubscription?.unsubscribe();
this.profileInfoSubscription?.unsubscribe();
this.irisSettingsSubscription?.unsubscribe();
this.submissionPolicySubscription?.unsubscribe();
this.buildLogsSubscription?.unsubscribe();
this.exerciseStatisticsSubscription?.unsubscribe();
}

/**
* <strong>BE CAREFUL WHEN CALLING THIS METHOD!</strong><br>
* This method can cause child components to re-render, which can lead to re-initializations resulting
* in unnecessary requests putting load on the server.
*
* <strong>When adding a new call to this method, make sure that no duplicated and unnecessary requests are made.</strong>
*/
getExerciseDetails(): DetailOverviewSection[] {
const exercise = this.programmingExercise;
exercise.buildConfig = this.programmingExerciseBuildConfig;
Expand Down Expand Up @@ -780,29 +788,37 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
return link;
}

loadGitDiffReport() {
this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => {
if (
gitDiffReport &&
(this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash ||
this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash)
) {
this.programmingExercise.gitDiffReport = gitDiffReport;
gitDiffReport.programmingExercise = this.programmingExercise;
this.addedLineCount =
gitDiffReport.entries
?.map((entry) => entry.lineCount)
.filter((lineCount) => lineCount)
.map((lineCount) => lineCount!)
.reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0;
this.removedLineCount =
gitDiffReport.entries
?.map((entry) => entry.previousLineCount)
.filter((lineCount) => lineCount)
.map((lineCount) => lineCount!)
.reduce((lineCount1, lineCount2) => lineCount1 + lineCount2, 0) ?? 0;
/**
*
* @param gitDiffReport
* @param updateDetailSections set to false when called from OnInit, as another method will take care to update the
* {@link exerciseDetailSections} to prevent unnecessary renderings and duplicated requests,
* see description of {@link getExerciseDetails}
*/
private processGitDiffReport(gitDiffReport: ProgrammingExerciseGitDiffReport | undefined, updateDetailSections: boolean = true): void {
const isGitDiffReportUpdated =
gitDiffReport &&
(this.programmingExercise.gitDiffReport?.templateRepositoryCommitHash !== gitDiffReport.templateRepositoryCommitHash ||
this.programmingExercise.gitDiffReport?.solutionRepositoryCommitHash !== gitDiffReport.solutionRepositoryCommitHash);
if (isGitDiffReportUpdated) {
this.programmingExercise.gitDiffReport = gitDiffReport;
gitDiffReport.programmingExercise = this.programmingExercise;

const calculateLineCount = (entries: { lineCount?: number; previousLineCount?: number }[] = [], key: 'lineCount' | 'previousLineCount') =>
entries.map((entry) => entry[key] ?? 0).reduce((sum, count) => sum + count, 0);

this.addedLineCount = calculateLineCount(gitDiffReport.entries, 'lineCount');
this.removedLineCount = calculateLineCount(gitDiffReport.entries, 'previousLineCount');

if (updateDetailSections) {
this.exerciseDetailSections = this.getExerciseDetails();
}
}
}

loadGitDiffReport() {
this.programmingExerciseService.getDiffReport(this.programmingExercise.id!).subscribe((gitDiffReport) => {
this.processGitDiffReport(gitDiffReport);
florian-glombik marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('DetailOverviewList', () => {
});

it('should initialize and destroy', () => {
component.sections = sections;
fixture.componentRef.setInput('sections', sections);
fixture.detectChanges();
expect(component.headlines).toStrictEqual([{ id: 'headline-1', translationKey: 'headline.1' }]);
expect(component.headlinesRecord).toStrictEqual({ 'headline.1': 'headline-1' });
Expand All @@ -67,7 +67,7 @@ describe('DetailOverviewList', () => {
});

it('should escape all falsy values', () => {
component.sections = [
fixture.componentRef.setInput('sections', [
{
headline: 'some-section',
details: [
Expand All @@ -81,7 +81,7 @@ describe('DetailOverviewList', () => {
},
],
},
];
]);
fixture.detectChanges();
const detailListTitleDOMElements = fixture.nativeElement.querySelectorAll('dt[id^=detail-title]');
expect(detailListTitleDOMElements).toHaveLength(1);
Expand Down
Loading
Loading