More Stat collection (#1337)

* Ensure that Scan Series triggers a file analysis task.

* Tweaked concurrency for Analyze Files

* Implemented new stats tracking for upcoming performance release.
This commit is contained in:
Joseph Milazzo 2022-06-27 10:23:32 -05:00 committed by GitHub
parent 77cd1bc80a
commit c809597cf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 157 additions and 9 deletions

View File

@ -2,49 +2,122 @@
namespace API.DTOs.Stats namespace API.DTOs.Stats
{ {
/// <summary>
/// Represents information about a Kavita Installation
/// </summary>
public class ServerInfoDto public class ServerInfoDto
{ {
/// <summary>
/// Unique Id that represents a unique install
/// </summary>
public string InstallId { get; set; } public string InstallId { get; set; }
public string Os { get; set; } public string Os { get; set; }
/// <summary>
/// If the Kavita install is using Docker
/// </summary>
public bool IsDocker { get; set; } public bool IsDocker { get; set; }
/// <summary>
/// Version of .NET instance is running
/// </summary>
public string DotnetVersion { get; set; } public string DotnetVersion { get; set; }
/// <summary>
/// Version of Kavita
/// </summary>
public string KavitaVersion { get; set; } public string KavitaVersion { get; set; }
/// <summary>
/// Number of Cores on the instance
/// </summary>
public int NumOfCores { get; set; } public int NumOfCores { get; set; }
/// <summary>
/// The number of libraries on the instance
/// </summary>
public int NumberOfLibraries { get; set; } public int NumberOfLibraries { get; set; }
/// <summary>
/// Does any user have bookmarks
/// </summary>
public bool HasBookmarks { get; set; } public bool HasBookmarks { get; set; }
/// <summary> /// <summary>
/// The site theme the install is using /// The site theme the install is using
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public string ActiveSiteTheme { get; set; } public string ActiveSiteTheme { get; set; }
/// <summary> /// <summary>
/// The reading mode the main user has as a preference /// The reading mode the main user has as a preference
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public ReaderMode MangaReaderMode { get; set; } public ReaderMode MangaReaderMode { get; set; }
/// <summary> /// <summary>
/// Number of users on the install /// Number of users on the install
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfUsers { get; set; } public int NumberOfUsers { get; set; }
/// <summary> /// <summary>
/// Number of collections on the install /// Number of collections on the install
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfCollections { get; set; } public int NumberOfCollections { get; set; }
/// <summary> /// <summary>
/// Number of reading lists on the install (Sum of all users) /// Number of reading lists on the install (Sum of all users)
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfReadingLists { get; set; } public int NumberOfReadingLists { get; set; }
/// <summary> /// <summary>
/// Is OPDS enabled /// Is OPDS enabled
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public bool OPDSEnabled { get; set; } public bool OPDSEnabled { get; set; }
/// <summary> /// <summary>
/// Total number of files in the instance /// Total number of files in the instance
/// </summary> /// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int TotalFiles { get; set; } public int TotalFiles { get; set; }
/// <summary>
/// Total number of Genres in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalGenres { get; set; }
/// <summary>
/// Total number of People in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalPeople { get; set; }
/// <summary>
/// Is this instance storing bookmarks as WebP
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool StoreBookmarksAsWebP { get; set; }
/// <summary>
/// Number of users on this instance using Card Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnCardLayout { get; set; }
/// <summary>
/// Number of users on this instance using List Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnListLayout { get; set; }
/// <summary>
/// Max number of Series for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxSeriesInALibrary { get; set; }
/// <summary>
/// Max number of Volumes for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxVolumesInASeries { get; set; }
/// <summary>
/// Max number of Chapters for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxChaptersInASeries { get; set; }
/// <summary>
/// Does this instance have relationships setup between series
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool UsingSeriesRelationships { get; set; }
} }
} }

View File

@ -18,6 +18,7 @@ public interface IGenreRepository
Task<IList<GenreTagDto>> GetAllGenreDtosAsync(); Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds); Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
Task<int> GetCountAsync();
} }
public class GenreRepository : IGenreRepository public class GenreRepository : IGenreRepository
@ -72,6 +73,11 @@ public class GenreRepository : IGenreRepository
.ToListAsync(); .ToListAsync();
} }
public async Task<int> GetCountAsync()
{
return await _context.Genre.CountAsync();
}
public async Task<IList<Genre>> GetAllGenresAsync() public async Task<IList<Genre>> GetAllGenresAsync()
{ {
return await _context.Genre.ToListAsync(); return await _context.Genre.ToListAsync();

View File

@ -16,6 +16,7 @@ public interface IPersonRepository
Task<IList<Person>> GetAllPeople(); Task<IList<Person>> GetAllPeople();
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false); Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds); Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
Task<int> GetCountAsync();
} }
public class PersonRepository : IPersonRepository public class PersonRepository : IPersonRepository
@ -72,6 +73,11 @@ public class PersonRepository : IPersonRepository
.ToListAsync(); .ToListAsync();
} }
public async Task<int> GetCountAsync()
{
return await _context.Person.CountAsync();
}
public async Task<IList<Person>> GetAllPeople() public async Task<IList<Person>> GetAllPeople()
{ {

View File

@ -57,6 +57,7 @@ public interface IUserRepository
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId); Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
Task<bool> HasAccessToLibrary(int libraryId, int userId); Task<bool> HasAccessToLibrary(int libraryId, int userId);
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags);
} }
public class UserRepository : IUserRepository public class UserRepository : IUserRepository
@ -246,6 +247,12 @@ public class UserRepository : IUserRepository
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId)); .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
} }
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags)
{
var query = AddIncludesToQuery(_context.Users.AsQueryable(), includeFlags);
return await query.ToListAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync() public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{ {
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);

View File

@ -18,7 +18,7 @@ namespace API.Services.Tasks.Metadata;
public interface IWordCountAnalyzerService public interface IWordCountAnalyzerService
{ {
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
Task ScanLibrary(int libraryId, bool forceUpdate = false); Task ScanLibrary(int libraryId, bool forceUpdate = false);
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true); Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true);
} }
@ -46,7 +46,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ScanLibrary(int libraryId, bool forceUpdate = false) public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();

View File

@ -191,6 +191,7 @@ public class ScannerService : IScannerService
await CleanupDbEntities(); await CleanupDbEntities();
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false)); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false));
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, series.Id, false));
} }
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series) private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)

View File

@ -4,13 +4,15 @@ using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Data.Repositories;
using API.DTOs.Stats; using API.DTOs.Stats;
using API.DTOs.Theme;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
using Flurl.Http; using Flurl.Http;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers; using Kavita.Common.Helpers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services.Tasks; namespace API.Services.Tasks;
@ -24,12 +26,14 @@ public class StatsService : IStatsService
{ {
private readonly ILogger<StatsService> _logger; private readonly ILogger<StatsService> _logger;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _context;
private const string ApiUrl = "https://stats.kavitareader.com"; private const string ApiUrl = "https://stats.kavitareader.com";
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork) public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context)
{ {
_logger = logger; _logger = logger;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_context = context;
FlurlHttp.ConfigureClient(ApiUrl, cli => FlurlHttp.ConfigureClient(ApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
@ -102,6 +106,8 @@ public class StatsService : IStatsService
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId); var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var serverInfo = new ServerInfoDto var serverInfo = new ServerInfoDto
{ {
InstallId = installId.Value, InstallId = installId.Value,
@ -114,11 +120,24 @@ public class StatsService : IStatsService
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(), NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(), NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(), NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(),
OPDSEnabled = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds, OPDSEnabled = serverSettings.EnableOpds,
NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsers()).Count(), NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsers()).Count(),
TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(), TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(),
TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(),
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
MaxVolumesInASeries = await MaxVolumesInASeries(),
MaxChaptersInASeries = await MaxChaptersInASeries(),
}; };
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
serverInfo.UsersOnCardLayout =
usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.Cards);
serverInfo.UsersOnListLayout =
usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.List);
var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault(); var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault();
if (firstAdminUser != null) if (firstAdminUser != null)
@ -132,4 +151,40 @@ public class StatsService : IStatsService
return serverInfo; return serverInfo;
} }
private Task<bool> GetIfUsingSeriesRelationship()
{
return _context.SeriesRelation.AnyAsync();
}
private Task<int> MaxSeriesInAnyLibrary()
{
return _context.Series
.Select(s => new
{
LibraryId = s.LibraryId,
Count = _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series).Count()
})
.MaxAsync(d => d.Count);
}
private Task<int> MaxVolumesInASeries()
{
return _context.Volume
.Select(v => new
{
v.SeriesId,
Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes).Count()
})
.MaxAsync(d => d.Count);
}
private Task<int> MaxChaptersInASeries()
{
return _context.Series
.MaxAsync(s => s.Volumes
.Where(v => v.Number == 0)
.SelectMany(v => v.Chapters)
.Count());
}
} }