add: собираем агрегированную информацию по настройкам типографа, процессорному времени и выводим процессорное время на фронет.
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m35s
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m35s
This commit is contained in:
@@ -274,9 +274,12 @@
|
|||||||
<label class="form-label fw-bold small text-muted ls-1 mb-0">
|
<label class="form-label fw-bold small text-muted ls-1 mb-0">
|
||||||
<i class="bi bi-code-slash me-1"></i> Результат обработки:
|
<i class="bi bi-code-slash me-1"></i> Результат обработки:
|
||||||
</label>
|
</label>
|
||||||
<button id="btn-copy" class="btn btn-sm btn-outline-secondary d-none" title="Копировать в буфер обмена">
|
<div class="d-flex align-items-center">
|
||||||
<i class="bi bi-clipboard me-1"></i> Копировать в буфер обмена
|
<span id="processing-time" class="small text-muted me-3 nowrap"></span>
|
||||||
</button>
|
<button id="btn-copy" class="btn btn-sm btn-outline-secondary d-none" title="Копировать в буфер обмена">
|
||||||
|
<i class="bi bi-clipboard me-1"></i> Копировать в буфер обмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cm-result-wrapper" class="result-box p-0"></div>
|
<div id="cm-result-wrapper" class="result-box p-0"></div>
|
||||||
<div id="result-area" style="display: none;"></div>
|
<div id="result-area" style="display: none;"></div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from etpgrf.typograph import Typographer
|
|||||||
from etpgrf.layout import LayoutProcessor
|
from etpgrf.layout import LayoutProcessor
|
||||||
from etpgrf.hyphenation import Hyphenator
|
from etpgrf.hyphenation import Hyphenator
|
||||||
from .models import DailyStat
|
from .models import DailyStat
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
@@ -131,39 +132,71 @@ def process_text(request):
|
|||||||
'sanitizer': sanitizer_option,
|
'sanitizer': sanitizer_option,
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- ДИАГНОСТИКА ---
|
# Обрабатываем текст с замером времени
|
||||||
# print("Typographer options:", options)
|
start_time = time.perf_counter()
|
||||||
# -------------------
|
# Создаем экземпляр типографа и передаем настройки в него
|
||||||
|
|
||||||
# Создаем экземпляр типографа
|
|
||||||
typo = Typographer(**options)
|
typo = Typographer(**options)
|
||||||
|
# Обрабатываем текст в Типографе
|
||||||
# Обрабатываем текст
|
|
||||||
processed = typo.process(text)
|
processed = typo.process(text)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
|
duration_ms = (end_time - start_time) * 1000
|
||||||
|
|
||||||
# --- СБОР СТАТИСТИКИ ---
|
# --- СБОР СТАТИСТИКИ ---
|
||||||
try:
|
try:
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
stat, created = DailyStat.objects.get_or_create(date=today)
|
stat, created = DailyStat.objects.get_or_create(date=today)
|
||||||
|
|
||||||
# Обновляем атомарные поля
|
# 1. Атомарное обновление счетчиков
|
||||||
DailyStat.objects.filter(pk=stat.pk).update(
|
DailyStat.objects.filter(pk=stat.pk).update(
|
||||||
process_requests=F('process_requests') + 1,
|
process_requests=F('process_requests') + 1,
|
||||||
chars_in=F('chars_in') + len(text),
|
chars_in=F('chars_in') + len(text),
|
||||||
chars_out=F('chars_out') + len(processed),
|
chars_out=F('chars_out') + len(processed),
|
||||||
# total_processing_time_ms мы пока не считаем, чтобы не усложнять
|
total_processing_time_ms=F('total_processing_time_ms') + duration_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
# JSON с настройками пока не пишем, чтобы не усложнять (как договаривались)
|
# 2. Обновление JSON с настройками (не атомарно, а значит при высокой нагрузке возможны потери данных)
|
||||||
|
# Перечитываем объект, чтобы получить актуальный JSON
|
||||||
|
stat.refresh_from_db()
|
||||||
|
current_stats = stat.settings_stats
|
||||||
|
|
||||||
|
def inc_stat(key, value):
|
||||||
|
k = f"{key}:{value}"
|
||||||
|
current_stats[k] = current_stats.get(k, 0) + 1
|
||||||
|
|
||||||
|
# Собираем статистику по опциям
|
||||||
|
# langs может быть строкой или списком
|
||||||
|
lang_val = options['langs']
|
||||||
|
if isinstance(lang_val, list):
|
||||||
|
lang_val = lang_val[0] if lang_val else 'ru'
|
||||||
|
inc_stat('lang', lang_val)
|
||||||
|
|
||||||
|
inc_stat('mode', options['mode'])
|
||||||
|
|
||||||
|
if options['quotes']: inc_stat('feature', 'quotes')
|
||||||
|
if layout_enabled: inc_stat('feature', 'layout')
|
||||||
|
if options['unbreakables']: inc_stat('feature', 'unbreakables')
|
||||||
|
if hyphenation_enabled: inc_stat('feature', 'hyphenation')
|
||||||
|
if options['symbols']: inc_stat('feature', 'symbols')
|
||||||
|
if hanging_enabled: inc_stat('feature', 'hanging')
|
||||||
|
if sanitizer_enabled: inc_stat('feature', 'sanitizer')
|
||||||
|
|
||||||
|
stat.settings_stats = current_stats
|
||||||
|
stat.save(update_fields=['settings_stats'])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Stat error: {e}")
|
print(f"Stat error: {e}")
|
||||||
# -----------------------
|
# -----------------------
|
||||||
|
|
||||||
return render(
|
response = render(
|
||||||
request,
|
request,
|
||||||
template_name='typograph/result_fragment.html',
|
template_name='typograph/result_fragment.html',
|
||||||
context={'processed_text': processed}
|
context={'processed_text': processed}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Добавляем заголовок с временем обработки (с запятой вместо точки)
|
||||||
|
response['X-Processing-Time'] = f"{duration_ms:.4f}".replace('.', ',')
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
return HttpResponse(status=405)
|
return HttpResponse(status=405)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
const resultWrapper = document.getElementById('cm-result-wrapper');
|
const resultWrapper = document.getElementById('cm-result-wrapper');
|
||||||
const btnCopy = document.getElementById('btn-copy');
|
const btnCopy = document.getElementById('btn-copy');
|
||||||
const sourceTextarea = document.querySelector('textarea[name="text"]');
|
const sourceTextarea = document.querySelector('textarea[name="text"]');
|
||||||
|
const processingTimeSpan = document.getElementById('processing-time');
|
||||||
|
|
||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
@@ -100,6 +101,17 @@ document.body.addEventListener('htmx:afterSwap', function (evt) {
|
|||||||
if (btnCopy) {
|
if (btnCopy) {
|
||||||
btnCopy.classList.remove('d-none');
|
btnCopy.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Показываем время обработки из заголовка
|
||||||
|
if (processingTimeSpan) {
|
||||||
|
const time = evt.detail.xhr.getResponseHeader('X-Processing-Time');
|
||||||
|
if (time) {
|
||||||
|
processingTimeSpan.innerHTML = `<i class="bi bi-cpu me-1"></i>${time} ms`;
|
||||||
|
processingTimeSpan.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
processingTimeSpan.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,6 +121,9 @@ if (sourceTextarea) {
|
|||||||
if (btnCopy) {
|
if (btnCopy) {
|
||||||
btnCopy.classList.add('d-none');
|
btnCopy.classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
if (processingTimeSpan) {
|
||||||
|
processingTimeSpan.innerText = '';
|
||||||
|
}
|
||||||
// Сбрасываем редактор на плейсхолдер
|
// Сбрасываем редактор на плейсхолдер
|
||||||
if (resultView.state.doc.toString() !== PLACEHOLDER_TEXT) {
|
if (resultView.state.doc.toString() !== PLACEHOLDER_TEXT) {
|
||||||
resultView.dispatch({
|
resultView.dispatch({
|
||||||
|
|||||||
Reference in New Issue
Block a user