perf(overview): Disable canvas generation on large torrents (#947)

This commit is contained in:
Rémi Marseault 2023-07-09 14:27:10 +02:00 committed by GitHub
parent d1fda8155d
commit b56caef1db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 132 additions and 71 deletions

View file

@ -40,18 +40,31 @@
<v-checkbox v-model="settings.openSideBarOnStart" hide-details class="ma-0 pa-0" :label="$t('modals.settings.vueTorrent.general.openSideBarOnStart')" />
</v-list-item>
<v-list-item class="mb-3">
<v-list-item>
<v-checkbox v-model="settings.showShutdownButton" hide-details class="ma-0 pa-0" :label="$t('modals.settings.vueTorrent.general.showShutdownButton')" />
</v-list-item>
<v-list-item class="mb-3">
<v-text-field
v-model="settings.refreshInterval"
type="number"
dense
outlined
:hint="$t('modals.settings.vueTorrent.general.refreshIntervalHint')"
:label="$t('modals.settings.vueTorrent.general.refreshInterval')" />
<v-list-item class="my-2">
<v-row>
<v-col cols="12" sm="6" class="mb-n4">
<v-text-field
v-model="settings.refreshInterval"
type="number"
dense
outlined
:hint="$t('modals.settings.vueTorrent.general.refreshIntervalHint')"
:label="$t('modals.settings.vueTorrent.general.refreshInterval')" />
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="settings.torrentPieceCountRenderThreshold"
type="number"
dense
outlined
:hint="$t('modals.settings.vueTorrent.general.torrentPieceCountRenderThresholdHint')"
:label="$t('modals.settings.vueTorrent.general.torrentPieceCountRenderThreshold')" />
</v-col>
</v-row>
</v-list-item>
<v-list-item class="mb-3">
@ -140,14 +153,14 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { mapState, mapGetters } from 'vuex'
import { Qbit } from '@/services/qbit'
import { LOCALES } from '@/lang/locales'
import { General } from '@/mixins'
import { TitleOptions } from '@/enums/vuetorrent'
import {defineComponent} from 'vue'
import {mapGetters, mapState} from 'vuex'
import {Qbit} from '@/services/qbit'
import {LOCALES} from '@/lang/locales'
import {General} from '@/mixins'
import {TitleOptions} from '@/enums/vuetorrent'
import Ajv from 'ajv'
import { StoreStateSchema } from '@/schemas'
import {StoreStateSchema} from '@/schemas'
import WebUISettings from '@/types/vuetorrent/WebUISettings'
export default defineComponent({

View file

@ -3,7 +3,7 @@
<v-row>
<v-col cols="12" md="6">
<v-card flat>
<v-card-title>{{ torrent.name }}</v-card-title>
<v-card-title class="overflow-wrap">{{ torrent.name }}</v-card-title>
<v-card-subtitle>
<div v-for="commentPart in splitString(comment)" :key="commentPart">
<a v-if="stringContainsUrl(commentPart)" target="_blank" :href="commentPart">{{ commentPart }}</a>
@ -15,25 +15,31 @@
<v-card-text>
<v-row>
<v-col cols="4" md="3">
<v-progress-circular v-if="torrent?.state === TorrentState.METADATA" indeterminate :size="100" color="torrent-metadata">{{ $t('modals.detail.pageOverview.fetchingMetadata') }}</v-progress-circular>
<v-progress-circular v-if="isFetchingMetadata" indeterminate :size="100" color="torrent-metadata">{{ $t('modals.detail.pageOverview.fetchingMetadata') }}</v-progress-circular>
<v-progress-circular v-else-if="torrent?.progress === 100" :size="100" :width="15" :value="100" color="torrent-seeding">
<v-icon color="torrent-seeding">{{ mdiCheck }}</v-icon>
</v-progress-circular>
<v-progress-circular v-else :rotate="-90" :size="100" :width="15" :value="torrent?.progress ?? 0" color="accent">{{ torrent.progress ?? 0 }} %</v-progress-circular>
</v-col>
<v-col cols="8" md="9" class="d-flex align-center justify-center flex-column">
<div>
<div v-if="shouldRenderPieceStates">
<canvas id="pieceStates" width="0" height="1" />
</div>
<div>
<div v-else-if="isFetchingMetadata">
<span>
{{ torrentPieceOwned }} / {{ torrentPieceCount }}
({{ torrentPieceSize | getDataValue }} {{ torrentPieceSize | getDataUnit }})
{{ $t('modals.detail.pageOverview.waitingForMetadata') }}
</span>
</div>
<div v-else>
<span>{{ $t('modals.detail.pageOverview.disabledCanvas') }}</span>
</div>
<div v-if="torrentPieceCount !== -1">
<span>{{ torrentPieceOwned }} / {{ torrentPieceCount }} ({{ torrentPieceSize | getData }})</span>
</div>
<div>
<v-icon>{{ mdiArrowDown }}</v-icon>
{{ torrent?.dlspeed | networkSpeed }}
<v-icon>{{ mdiArrowUp }}</v-icon>
{{ torrent?.upspeed | networkSpeed }}
</div>
@ -41,13 +47,15 @@
</v-row>
<v-row>
<v-col cols="6">
{{ $t('torrent.properties.save_path') }}:<br/>
{{ torrent.savePath }}
<div>{{ $t('torrent.properties.save_path') }}:</div>
<div>{{ torrent.savePath }}</div>
</v-col>
<v-col cols="6">
{{ $t('modals.detail.pageOverview.fileCount') }}:<br/>
{{ selectedFileCount }} / {{ torrentFileCount }}
<span v-if="selectedFileCount === 1">({{ torrentFileName }})</span>
<div>{{ $t('modals.detail.pageOverview.fileCount') }}:</div>
<div>
{{ selectedFileCount }} / {{torrentFileCount }}
<span v-if="selectedFileCount === 1">({{ torrentFileName }})</span>
</div>
</v-col>
</v-row>
</v-card-text>
@ -58,62 +66,67 @@
<v-card-text>
<v-row>
<v-col cols="6">
{{ $t('torrent.properties.status') }}:
<v-chip small :class="`${torrentStateClass} white--text caption ml-2`">{{ torrent.state }}</v-chip>
<div>
{{ $t('torrent.properties.status') }}:
</div>
<v-chip small :class="torrentStateClass" class="white--text caption">{{ torrent.state }}</v-chip>
</v-col>
<v-col cols="6">
{{ $t('torrent.properties.category') }}:
<v-chip small class="upload white--text caption ml-2">
<div>
{{ $t('torrent.properties.category') }}:
</div>
<v-chip small class="upload white--text caption">
{{ torrent.category.length ? torrent.category : $t('navbar.filters.uncategorized') }}
</v-chip>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
{{ $t('torrent.properties.tracker') }}:
<v-chip small class="moving white--text caption ml-2">
{{ this.torrent?.tracker ? getDomainBody(this.torrent?.tracker) : $t('navbar.filters.untracked') }}
</v-chip>
<div>
{{ $t('torrent.properties.tracker') }}:
</div>
<v-chip small class="moving white--text caption">{{ this.torrent?.tracker ? getDomainBody(this.torrent?.tracker) : $t('navbar.filters.untracked') }}</v-chip>
</v-col>
<v-col cols="6">
{{ $t('torrent.properties.tags') }}:
<v-chip v-if="torrent?.tags" v-for="tag in torrent.tags" :key="tag" small
class="tags white--text caption ml-2">{{ tag }}
<div class="d-flex flex-wrap chipgap">
{{ $t('torrent.properties.tags') }}:
</div>
<v-chip v-if="torrent?.tags" v-for="tag in torrent.tags" :key="tag" small class="tags white--text caption">
{{ tag }}
</v-chip>
<v-chip v-if="!torrent?.tags || torrent.tags.length === 0" small class="tags white--text caption ml-2">
<v-chip v-if="!torrent?.tags || torrent.tags.length === 0" small class="tags white--text caption">
{{ $t('navbar.filters.untagged') }}
</v-chip>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
{{ $t('modals.detail.pageOverview.selectedFileSize') }}:<br/>
{{ torrent?.size | getDataValue }} {{ torrent?.size | getDataUnit }} /
{{ torrent?.total_size | getDataValue }} {{ torrent?.total_size | getDataUnit }}
<div>{{ $t('modals.detail.pageOverview.selectedFileSize') }}:</div>
<div>{{ torrent?.size | getData }} / {{ torrent?.total_size | getData }}</div>
</v-col>
<v-col cols="6">
{{ $t('ratio') }}:<br/>
{{ torrent?.ratio }}
<div>{{ $t('ratio') }}:</div>
<div>{{ torrent?.ratio }}</div>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
{{ $t('downloaded') }}:<br/>
{{ torrent?.downloaded | getDataValue }} {{ torrent?.downloaded | getDataUnit }}
<div>{{ $t('downloaded') }}:</div>
<div>{{ torrent?.downloaded | getData }}</div>
</v-col>
<v-col cols="6">
{{ $t('uploaded') }}:<br/>
{{ torrent?.uploaded | getDataValue }} {{ torrent?.uploaded | getDataUnit }}
<div>{{ $t('uploaded') }}:</div>
<div>{{ torrent?.uploaded | getData }}</div>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
{{ $t('modals.detail.pageOverview.dlSpeedAverage') }}:<br/>
{{ downloadSpeedAvg | networkSpeed }}
<div>{{ $t('modals.detail.pageOverview.dlSpeedAverage') }}:</div>
<div>{{ downloadSpeedAvg | networkSpeed }}</div>
</v-col>
<v-col cols="6">
{{ $t('modals.detail.pageOverview.upSpeedAverage') }}:<br/>
{{ uploadSpeedAvg | networkSpeed }}
<div>{{ $t('modals.detail.pageOverview.upSpeedAverage') }}:</div>
<div>{{ uploadSpeedAvg | networkSpeed }}</div>
</v-col>
</v-row>
</v-card-text>
@ -189,13 +202,21 @@ export default defineComponent({
},
torrentStateClass() {
return this.torrent?.state ? this.torrent.state.toLowerCase() : ''
},
isFetchingMetadata() {
return this.torrent?.state === TorrentState.METADATA
},
shouldRenderPieceStates() {
return !this.isFetchingMetadata
&& this.torrentPieceCount > 0
&& this.torrentPieceCount < this.webuiSettings.torrentPieceCountRenderThreshold
}
},
watch: {
torrent() {
this.getTorrentProperties()
if (this.torrentPieceCount < 5000) {
this.renderTorrentPieceStates()
async torrent() {
await this.getTorrentProperties()
if (this.shouldRenderPieceStates) {
await this.renderTorrentPieceStates()
}
}
},
@ -205,7 +226,6 @@ export default defineComponent({
const props = await qbit.getTorrentProperties(this.torrent?.hash as string)
this.comment = props.comment
this.createdBy = props.created_by
// @ts-expect-error: TS2339: Property 'dateFormat' does not exist on type 'never'.
this.creationDate = dayjs(props.creation_date * 1000).format(this.webuiSettings.dateFormat)
this.downloadSpeedAvg = props.dl_speed_avg
this.isPrivateTorrent = props.is_private
@ -220,10 +240,9 @@ export default defineComponent({
const files = await qbit.getTorrentFiles(this.torrent?.hash as string)
const pieces = await qbit.getTorrentPieceStates(this.torrent?.hash as string)
if (!pieces) return
// Source: https://github.com/qbittorrent/qBittorrent/blob/6229b817300344759139d2fedbd59651065a561d/src/webui/www/private/scripts/prop-general.js#L230
canvas.width = pieces.length
canvas.width = pieces.length || -1
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.clearRect(0, 0, canvas.width, canvas.height)
@ -318,4 +337,12 @@ canvas#pieceStates {
.v-card__title {
word-break: normal;
}
.chipgap {
gap: 4px;
}
.overflow-wrap {
overflow-wrap: anywhere;
}
</style>

View file

@ -73,6 +73,12 @@ export function getDataValue(data: number, precision: number = 2) {
Vue.filter('getDataValue', getDataValue)
export function getData(data: number, precision: number = 2) {
return `${getDataValue(data, precision)} ${getDataUnit(data)}`
}
Vue.filter('getData', getData)
export function titleCase(str: string): string {
if (str.length == 0) return str

View file

@ -271,6 +271,8 @@
"showShutdownButton": "Show shutdown button",
"refreshInterval": "qBittorrent API refresh interval",
"refreshIntervalHint": "In milliseconds",
"torrentPieceCountRenderThreshold": "Torrent piece count to disable rendering",
"torrentPieceCountRenderThresholdHint": "In milliseconds",
"currentVersion": "Current Version",
"qbittorrentVersion": "QBittorrent Version",
"registerMagnet": "Register magnet links",
@ -656,6 +658,8 @@
"tabTitleTagsCategories": "Tags & Categories",
"pageOverview": {
"fetchingMetadata": "Fetching...",
"waitingForMetadata": "Waiting for metadata...",
"disabledCanvas": "Canvas disabled to save performance",
"selectedFileSize": "Selected Files' Size",
"dlSpeedAverage": "Download Speed Average",
"upSpeedAverage": "Upload Speed Average",

View file

@ -1,6 +1,6 @@
import { JSONSchemaType } from 'ajv'
import { PersistentStoreState } from '@/types/vuetorrent'
import { DashboardProperty, TitleOptions } from '@/enums/vuetorrent'
import {JSONSchemaType} from 'ajv'
import {PersistentStoreState} from '@/types/vuetorrent'
import {DashboardProperty, TitleOptions} from '@/enums/vuetorrent'
export const StoreStateSchema: JSONSchemaType<PersistentStoreState> = {
type: 'object',
@ -43,6 +43,7 @@ export const StoreStateSchema: JSONSchemaType<PersistentStoreState> = {
openSideBarOnStart: { type: 'boolean' },
showShutdownButton: { type: 'boolean' },
refreshInterval: { type: 'number' },
torrentPieceCountRenderThreshold: { type: 'number' },
busyDesktopTorrentProperties: { $ref: '/schemas/desktopDashboardProperties' },
doneDesktopTorrentProperties: { $ref: '/schemas/desktopDashboardProperties' },
busyMobileCardProperties: { $ref: '/schemas/mobileDashboardProperties' },
@ -67,6 +68,7 @@ export const StoreStateSchema: JSONSchemaType<PersistentStoreState> = {
'openSideBarOnStart',
'showShutdownButton',
'refreshInterval',
'torrentPieceCountRenderThreshold',
'busyDesktopTorrentProperties',
'doneDesktopTorrentProperties',
'busyMobileCardProperties',

View file

@ -4,10 +4,10 @@ import VuexPersist from 'vuex-persist'
import actions from './actions'
import getters from './getters'
import mutations from './mutations'
import type { StoreState, PersistentStoreState } from '@/types/vuetorrent'
import { Status } from '@/models'
import { TitleOptions, DashboardProperty } from '@/enums/vuetorrent'
import { AppPreferences } from '@/types/qbit/models'
import type {PersistentStoreState, StoreState} from '@/types/vuetorrent'
import {Status} from '@/models'
import {DashboardProperty, TitleOptions} from '@/enums/vuetorrent'
import {AppPreferences} from '@/types/qbit/models'
const vuexPersist = new VuexPersist<PersistentStoreState>({
key: 'vuetorrent',
@ -136,13 +136,14 @@ export default new Vuex.Store<StoreState>({
openSideBarOnStart: true,
showShutdownButton: true,
refreshInterval: 2000,
torrentPieceCountRenderThreshold: 5000,
busyDesktopTorrentProperties: JSON.parse(JSON.stringify(desktopPropertiesTemplate)),
doneDesktopTorrentProperties: JSON.parse(JSON.stringify(desktopPropertiesTemplate)),
busyMobileCardProperties: JSON.parse(JSON.stringify(mobilePropertiesTemplate)),
doneMobileCardProperties: JSON.parse(JSON.stringify(mobilePropertiesTemplate))
}
},
// @ts-expect-error
//@ts-expect-error: TS2322: Type '...' is not assignable to type 'ActionTree<StoreState, StoreState>'.
actions: {
...actions
},

View file

@ -1,4 +1,4 @@
import type { DashboardProperty, TitleOptions } from '@/enums/vuetorrent'
import type {DashboardProperty, TitleOptions} from '@/enums/vuetorrent'
export class TorrentProperty {
name: DashboardProperty
@ -29,6 +29,7 @@ export default interface WebUISettings {
openSideBarOnStart: boolean
showShutdownButton: boolean
refreshInterval: number
torrentPieceCountRenderThreshold: number
busyDesktopTorrentProperties: TorrentProperty[]
doneDesktopTorrentProperties: TorrentProperty[]
busyMobileCardProperties: TorrentProperty[]

View file

@ -30,11 +30,18 @@ exports[`General > render correctly 1`] = `
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\">
<v-checkbox-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" hidedetails=\\"true\\" ripple=\\"true\\" valuecomparator=\\"[Function]\\" indeterminateicon=\\"$checkboxIndeterminate\\" officon=\\"$checkboxOff\\" onicon=\\"$checkboxOn\\" class=\\"ma-0 pa-0\\"></v-checkbox-stub>
</v-list-item-stub>
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"mb-3\\">
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\">
<v-checkbox-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" hidedetails=\\"true\\" ripple=\\"true\\" valuecomparator=\\"[Function]\\" indeterminateicon=\\"$checkboxIndeterminate\\" officon=\\"$checkboxOff\\" onicon=\\"$checkboxOn\\" class=\\"ma-0 pa-0\\"></v-checkbox-stub>
</v-list-item-stub>
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"mb-3\\">
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"my-2\\">
<v-row-stub data-v-7da6d3e2=\\"\\" tag=\\"div\\">
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" tag=\\"div\\" class=\\"mb-n4\\">
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
</v-col-stub>
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" tag=\\"div\\">
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
</v-col-stub>
</v-row-stub>
</v-list-item-stub>
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"mb-3\\">
<v-row-stub data-v-7da6d3e2=\\"\\" tag=\\"div\\">