From cb3929e499ad128d0cf8655de5a8590ede156caa Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 8 Sep 2021 16:26:09 -0700 Subject: [PATCH] Performance Improvements (#568) * Refactored the performance of GetChapter/BookInfo API to have a 10x speed improvement and to use common code, rather than duplicating code. Removed an api param that is no longer needed. * Book reader now has dedicated buttons to jump to next/prev chapter as well as through page buttons --- API/Controllers/BookController.cs | 31 +++++++------ API/Controllers/ReaderController.cs | 27 +++++------- API/DTOs/Reader/BookInfoDto.cs | 7 ++- API/DTOs/Reader/ChapterInfoDto.cs | 2 +- API/DTOs/Reader/IChapterInfoDto.cs | 19 ++++++++ API/Data/Repositories/ChapterRepository.cs | 43 +++++++++++++++++++ .../Repositories/IChapterRepository.cs | 2 + API/Services/Tasks/VersionUpdaterService.cs | 6 ++- UI/Web/src/app/_services/reader.service.ts | 4 +- .../book-reader/book-reader.component.html | 8 ++-- .../manga-reader/manga-reader.component.ts | 6 +-- 11 files changed, 112 insertions(+), 43 deletions(-) create mode 100644 API/DTOs/Reader/IChapterInfoDto.cs diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index dbae6aa09..1a42505c7 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -35,29 +35,28 @@ namespace API.Controllers [HttpGet("{chapterId}/book-info")] public async Task> GetBookInfo(int chapterId) { - // PERF: Write this in one DB call - This does not meet NFR - var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId); - var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId); - if (volume == null) return BadRequest("Could not find Volume"); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); - if (series == null) return BadRequest("Series could not be found"); - + var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); var bookTitle = string.Empty; - if (series.Format == MangaFormat.Epub) + if (dto.SeriesFormat == MangaFormat.Epub) { - using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First(); + using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath); bookTitle = book.Title; } - - return new BookInfoDto() + return Ok(new BookInfoDto() { + ChapterNumber = dto.ChapterNumber, + VolumeNumber = dto.VolumeNumber, + VolumeId = dto.VolumeId, BookTitle = bookTitle, - VolumeId = chapter.VolumeId, - SeriesFormat = series.Format, - SeriesId = series.Id, - LibraryId = series.LibraryId, - }; + SeriesName = dto.SeriesName, + SeriesFormat = dto.SeriesFormat, + SeriesId = dto.SeriesId, + LibraryId = dto.LibraryId, + IsSpecial = dto.IsSpecial, + Pages = dto.Pages, + }); } [HttpGet("{chapterId}/book-resources")] diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 2690c8bdb..9578d8470 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -76,34 +76,29 @@ namespace API.Controllers /// /// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading. /// - /// Not used /// /// [HttpGet("chapter-info")] - public async Task> GetChapterInfo(int seriesId, int chapterId) + public async Task> GetChapterInfo(int chapterId) { - // PERF: Write this in one DB call - This does not meet NFR var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest("Could not find Chapter"); - var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId); - if (volume == null) return BadRequest("Could not find Volume"); + var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First(); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); - if (series == null) return BadRequest("Series could not be found"); return Ok(new ChapterInfoDto() { - ChapterNumber = chapter.Range, - VolumeNumber = volume.Number + string.Empty, - VolumeId = volume.Id, + ChapterNumber = dto.ChapterNumber, + VolumeNumber = dto.VolumeNumber, + VolumeId = dto.VolumeId, FileName = Path.GetFileName(mangaFile.FilePath), - SeriesName = series.Name, - SeriesFormat = series.Format, - SeriesId = series.Id, - LibraryId = series.LibraryId, - IsSpecial = chapter.IsSpecial, - Pages = chapter.Pages, + SeriesName = dto.SeriesName, + SeriesFormat = dto.SeriesFormat, + SeriesId = dto.SeriesId, + LibraryId = dto.LibraryId, + IsSpecial = dto.IsSpecial, + Pages = dto.Pages, }); } diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index 0404ab5e3..6705c9647 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -2,12 +2,17 @@ namespace API.DTOs.Reader { - public class BookInfoDto + public class BookInfoDto : IChapterInfoDto { public string BookTitle { get; set; } public int SeriesId { get; set; } public int VolumeId { get; set; } public MangaFormat SeriesFormat { get; set; } + public string SeriesName { get; set; } + public string ChapterNumber { get; set; } + public string VolumeNumber { get; set; } public int LibraryId { get; set; } + public int Pages { get; set; } + public bool IsSpecial { get; set; } } } diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 3ad1c96e2..ec512670d 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader { - public class ChapterInfoDto + public class ChapterInfoDto : IChapterInfoDto { public string ChapterNumber { get; set; } diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/API/DTOs/Reader/IChapterInfoDto.cs new file mode 100644 index 000000000..63b5c9a62 --- /dev/null +++ b/API/DTOs/Reader/IChapterInfoDto.cs @@ -0,0 +1,19 @@ +using API.Entities.Enums; +using Newtonsoft.Json; + +namespace API.DTOs.Reader +{ + public interface IChapterInfoDto + { + public int SeriesId { get; set; } + public int VolumeId { get; set; } + public MangaFormat SeriesFormat { get; set; } + public string SeriesName { get; set; } + public string ChapterNumber { get; set; } + public string VolumeNumber { get; set; } + public int LibraryId { get; set; } + public int Pages { get; set; } + public bool IsSpecial { get; set; } + + } +} diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 56839a97c..3ea93b776 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.DTOs.Reader; using API.Entities; using API.Interfaces.Repositories; using Microsoft.EntityFrameworkCore; @@ -30,5 +31,47 @@ namespace API.Data.Repositories } // TODO: Move over Chapter based queries here + + /// + /// Populates a partial IChapterInfoDto + /// + /// + public async Task GetChapterInfoDtoAsync(int chapterId) + { + return await _context.Chapter + .Where(c => c.Id == chapterId) + .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new + { + ChapterNumber = chapter.Range, + VolumeNumber = volume.Number, + VolumeId = volume.Id, + chapter.IsSpecial, + volume.SeriesId + }) + .Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new + { + data.ChapterNumber, + data.VolumeNumber, + data.VolumeId, + data.IsSpecial, + data.SeriesId, + SeriesFormat = series.Format, + SeriesName = series.Name, + series.LibraryId + }) + .Select(data => new BookInfoDto() + { + ChapterNumber = data.ChapterNumber, + VolumeNumber = data.VolumeNumber + string.Empty, + VolumeId = data.VolumeId, + IsSpecial = data.IsSpecial, + SeriesId =data.SeriesId, + SeriesFormat = data.SeriesFormat, + SeriesName = data.SeriesName, + LibraryId = data.LibraryId + }) + .AsNoTracking() + .SingleAsync(); + } } } diff --git a/API/Interfaces/Repositories/IChapterRepository.cs b/API/Interfaces/Repositories/IChapterRepository.cs index 8361a0a38..02ef94eed 100644 --- a/API/Interfaces/Repositories/IChapterRepository.cs +++ b/API/Interfaces/Repositories/IChapterRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using API.DTOs.Reader; using API.Entities; namespace API.Interfaces.Repositories @@ -8,5 +9,6 @@ namespace API.Interfaces.Repositories { void Update(Chapter chapter); Task> GetChaptersByIdsAsync(IList chapterIds); + Task GetChapterInfoDtoAsync(int chapterId); } } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 5ce551afe..a949a870e 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -23,6 +23,7 @@ namespace API.Services.Tasks /// Name of the Tag /// v0.4.3 /// + // ReSharper disable once InconsistentNaming public string Tag_Name { get; init; } /// /// Name of the Release @@ -35,6 +36,7 @@ namespace API.Services.Tasks /// /// Url of the release on Github /// + // ReSharper disable once InconsistentNaming public string Html_Url { get; init; } } @@ -53,8 +55,10 @@ namespace API.Services.Tasks private readonly IHubContext _messageHub; private readonly IPresenceTracker _tracker; private readonly Markdown _markdown = new MarkdownDeep.Markdown(); +#pragma warning disable S1075 private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest"; private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; +#pragma warning restore S1075 public VersionUpdaterService(ILogger logger, IHubContext messageHub, IPresenceTracker tracker) { @@ -95,7 +99,7 @@ namespace API.Services.Tasks if (updateVersion.Revision == -1) { - currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".")); + currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal)); } return new UpdateNotificationDto() diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 3ad5f8724..a69cb52e1 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -56,8 +56,8 @@ export class ReaderService { return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page; } - getChapterInfo(seriesId: number, chapterId: number) { - return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId); + getChapterInfo(chapterId: number) { + return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId); } saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) { diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html index eb96c4bc7..e06c77153 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html @@ -63,11 +63,13 @@
-
{{pageNum}}
-
+ +
{{pageNum}}
+
-
{{maxPages - 1}}
+
{{maxPages - 1}}
+

Table of Contents

diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index df6239772..c348acb42 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -378,7 +378,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { forkJoin({ progress: this.readerService.getProgress(this.chapterId), - chapterInfo: this.readerService.getChapterInfo(this.seriesId, this.chapterId), + chapterInfo: this.readerService.getChapterInfo(this.chapterId), bookmarks: this.readerService.getBookmarks(this.chapterId) }).pipe(take(1)).subscribe(results => { @@ -867,13 +867,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.pageNum >= this.maxPages - 10) { // Tell server to cache the next chapter if (this.nextChapterId > 0 && !this.nextChapterPrefetched) { - this.readerService.getChapterInfo(this.seriesId, this.nextChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => { this.nextChapterPrefetched = true; }); } } else if (this.pageNum <= 10) { if (this.prevChapterId > 0 && !this.prevChapterPrefetched) { - this.readerService.getChapterInfo(this.seriesId, this.prevChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => { this.prevChapterPrefetched = true; }); }