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
This commit is contained in:
Joseph Milazzo 2021-09-08 16:26:09 -07:00 committed by GitHub
parent 84fa617023
commit cb3929e499
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 112 additions and 43 deletions

View File

@ -35,29 +35,28 @@ namespace API.Controllers
[HttpGet("{chapterId}/book-info")] [HttpGet("{chapterId}/book-info")]
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId) public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{ {
// PERF: Write this in one DB call - This does not meet NFR var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
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 bookTitle = string.Empty; 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; bookTitle = book.Title;
} }
return Ok(new BookInfoDto()
return new BookInfoDto()
{ {
ChapterNumber = dto.ChapterNumber,
VolumeNumber = dto.VolumeNumber,
VolumeId = dto.VolumeId,
BookTitle = bookTitle, BookTitle = bookTitle,
VolumeId = chapter.VolumeId, SeriesName = dto.SeriesName,
SeriesFormat = series.Format, SeriesFormat = dto.SeriesFormat,
SeriesId = series.Id, SeriesId = dto.SeriesId,
LibraryId = series.LibraryId, LibraryId = dto.LibraryId,
}; IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
});
} }
[HttpGet("{chapterId}/book-resources")] [HttpGet("{chapterId}/book-resources")]

View File

@ -76,34 +76,29 @@ namespace API.Controllers
/// <summary> /// <summary>
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading. /// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
/// </summary> /// </summary>
/// <param name="seriesId">Not used</param>
/// <param name="chapterId"></param> /// <param name="chapterId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("chapter-info")] [HttpGet("chapter-info")]
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId) public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
{ {
// PERF: Write this in one DB call - This does not meet NFR
var chapter = await _cacheService.Ensure(chapterId); var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("Could not find Chapter"); if (chapter == null) return BadRequest("Could not find Chapter");
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId); var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
if (volume == null) return BadRequest("Could not find Volume");
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First(); 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() return Ok(new ChapterInfoDto()
{ {
ChapterNumber = chapter.Range, ChapterNumber = dto.ChapterNumber,
VolumeNumber = volume.Number + string.Empty, VolumeNumber = dto.VolumeNumber,
VolumeId = volume.Id, VolumeId = dto.VolumeId,
FileName = Path.GetFileName(mangaFile.FilePath), FileName = Path.GetFileName(mangaFile.FilePath),
SeriesName = series.Name, SeriesName = dto.SeriesName,
SeriesFormat = series.Format, SeriesFormat = dto.SeriesFormat,
SeriesId = series.Id, SeriesId = dto.SeriesId,
LibraryId = series.LibraryId, LibraryId = dto.LibraryId,
IsSpecial = chapter.IsSpecial, IsSpecial = dto.IsSpecial,
Pages = chapter.Pages, Pages = dto.Pages,
}); });
} }

View File

@ -2,12 +2,17 @@
namespace API.DTOs.Reader namespace API.DTOs.Reader
{ {
public class BookInfoDto public class BookInfoDto : IChapterInfoDto
{ {
public string BookTitle { get; set; } public string BookTitle { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int VolumeId { get; set; } public int VolumeId { get; set; }
public MangaFormat SeriesFormat { 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 LibraryId { get; set; }
public int Pages { get; set; }
public bool IsSpecial { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@
namespace API.DTOs.Reader namespace API.DTOs.Reader
{ {
public class ChapterInfoDto public class ChapterInfoDto : IChapterInfoDto
{ {
public string ChapterNumber { get; set; } public string ChapterNumber { get; set; }

View File

@ -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; }
}
}

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Reader;
using API.Entities; using API.Entities;
using API.Interfaces.Repositories; using API.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -30,5 +31,47 @@ namespace API.Data.Repositories
} }
// TODO: Move over Chapter based queries here // TODO: Move over Chapter based queries here
/// <summary>
/// Populates a partial IChapterInfoDto
/// </summary>
/// <returns></returns>
public async Task<IChapterInfoDto> 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();
}
} }
} }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Reader;
using API.Entities; using API.Entities;
namespace API.Interfaces.Repositories namespace API.Interfaces.Repositories
@ -8,5 +9,6 @@ namespace API.Interfaces.Repositories
{ {
void Update(Chapter chapter); void Update(Chapter chapter);
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds); Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
} }
} }

View File

@ -23,6 +23,7 @@ namespace API.Services.Tasks
/// Name of the Tag /// Name of the Tag
/// <example>v0.4.3</example> /// <example>v0.4.3</example>
/// </summary> /// </summary>
// ReSharper disable once InconsistentNaming
public string Tag_Name { get; init; } public string Tag_Name { get; init; }
/// <summary> /// <summary>
/// Name of the Release /// Name of the Release
@ -35,6 +36,7 @@ namespace API.Services.Tasks
/// <summary> /// <summary>
/// Url of the release on Github /// Url of the release on Github
/// </summary> /// </summary>
// ReSharper disable once InconsistentNaming
public string Html_Url { get; init; } public string Html_Url { get; init; }
} }
@ -53,8 +55,10 @@ namespace API.Services.Tasks
private readonly IHubContext<MessageHub> _messageHub; private readonly IHubContext<MessageHub> _messageHub;
private readonly IPresenceTracker _tracker; private readonly IPresenceTracker _tracker;
private readonly Markdown _markdown = new MarkdownDeep.Markdown(); 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 GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
#pragma warning restore S1075
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker) public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
{ {
@ -95,7 +99,7 @@ namespace API.Services.Tasks
if (updateVersion.Revision == -1) if (updateVersion.Revision == -1)
{ {
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".")); currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal));
} }
return new UpdateNotificationDto() return new UpdateNotificationDto()

View File

@ -56,8 +56,8 @@ export class ReaderService {
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page; return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
} }
getChapterInfo(seriesId: number, chapterId: number) { getChapterInfo(chapterId: number) {
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId); return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId);
} }
saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) { saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) {

View File

@ -63,11 +63,13 @@
</div> </div>
</div> </div>
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-1">{{pageNum}}</div> <button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-10" style="margin-top: 9px"> <div class="col-1" style="margin-top: 6px">{{pageNum}}</div>
<div class="col-8" style="margin-top: 15px">
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar> <ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div> </div>
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div> <div class="col-1 btn-icon" style="margin-top: 6px" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div> </div>
<div class="table-of-contents"> <div class="table-of-contents">
<h3>Table of Contents</h3> <h3>Table of Contents</h3>

View File

@ -378,7 +378,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
forkJoin({ forkJoin({
progress: this.readerService.getProgress(this.chapterId), 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) bookmarks: this.readerService.getBookmarks(this.chapterId)
}).pipe(take(1)).subscribe(results => { }).pipe(take(1)).subscribe(results => {
@ -867,13 +867,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.pageNum >= this.maxPages - 10) { if (this.pageNum >= this.maxPages - 10) {
// Tell server to cache the next chapter // Tell server to cache the next chapter
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) { 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; this.nextChapterPrefetched = true;
}); });
} }
} else if (this.pageNum <= 10) { } else if (this.pageNum <= 10) {
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) { 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; this.prevChapterPrefetched = true;
}); });
} }