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")]
public async Task<ActionResult<BookInfoDto>> 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")]

View File

@ -76,34 +76,29 @@ namespace API.Controllers
/// <summary>
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
/// </summary>
/// <param name="seriesId">Not used</param>
/// <param name="chapterId"></param>
/// <returns></returns>
[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);
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,
});
}

View File

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

View File

@ -2,7 +2,7 @@
namespace API.DTOs.Reader
{
public class ChapterInfoDto
public class ChapterInfoDto : IChapterInfoDto
{
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.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
/// <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.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<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
/// <example>v0.4.3</example>
/// </summary>
// ReSharper disable once InconsistentNaming
public string Tag_Name { get; init; }
/// <summary>
/// Name of the Release
@ -35,6 +36,7 @@ namespace API.Services.Tasks
/// <summary>
/// Url of the release on Github
/// </summary>
// ReSharper disable once InconsistentNaming
public string Html_Url { get; init; }
}
@ -53,8 +55,10 @@ namespace API.Services.Tasks
private readonly IHubContext<MessageHub> _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<VersionUpdaterService> logger, IHubContext<MessageHub> 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()

View File

@ -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<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId);
getChapterInfo(chapterId: number) {
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) {

View File

@ -63,11 +63,13 @@
</div>
</div>
<div class="row no-gutters">
<div class="col-1">{{pageNum}}</div>
<div class="col-10" style="margin-top: 9px">
<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-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>
</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 class="table-of-contents">
<h3>Table of Contents</h3>

View File

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