Skip to content

Commit 7f0fc14

Browse files
author
n3kosempai
committed
[release] solved slow loading permision per apps
1 parent d1ff15d commit 7f0fc14

9 files changed

Lines changed: 852 additions & 62 deletions

File tree

pnpm-lock.yaml

Lines changed: 488 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/lib.rs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ fn get_app_permissions(app_id: &str, is_flatpak: bool) -> Option<Vec<String>> {
128128
};
129129

130130
if !output.status.success() {
131-
println!("[DEBUG] Failed to get permissions for {}: command failed", app_id);
132131
return None;
133132
}
134133

@@ -138,39 +137,30 @@ fn get_app_permissions(app_id: &str, is_flatpak: bool) -> Option<Vec<String>> {
138137
let mut has_files = false;
139138
let mut has_storage = false;
140139

141-
println!("[DEBUG] Parsing permissions for {}:", app_id);
142-
143140
for line in stdout.lines() {
144141
let line = line.trim();
145142
if line.is_empty() || line.starts_with('[') {
146143
continue;
147144
}
148145

149-
println!("[DEBUG] Line: {}", line);
150-
151146
// Look for specific permissions we care about
152147
// Camera: devices=all (full device access) or specific video devices
153148
if !has_camera && line.contains("devices=") {
154149
let devices_part = line.split("devices=").nth(1).unwrap_or("");
155-
let devices_value = devices_part.split(';').next().unwrap_or("").trim();
156-
println!("[DEBUG] Found devices: '{}'", devices_value);
157-
// Only consider "all" as camera permission (dri is just GPU acceleration)
158-
if devices_value == "all" {
150+
// Check if "all" is in the devices list (e.g., "dri;all;" or "all;")
151+
if devices_part.split(';').any(|d| d.trim() == "all") {
159152
permissions.push("camera".to_string());
160153
has_camera = true;
161-
println!("[DEBUG] Added camera permission");
162154
}
163155
}
164156

165157
// Files: filesystems= with any value
166158
if !has_files && line.contains("filesystems=") {
167159
let filesystems_part = line.split("filesystems=").nth(1).unwrap_or("");
168160
let filesystems_value = filesystems_part.split(';').next().unwrap_or("").trim();
169-
println!("[DEBUG] Found filesystems: '{}'", filesystems_value);
170161
if !filesystems_value.is_empty() {
171162
permissions.push("files".to_string());
172163
has_files = true;
173-
println!("[DEBUG] Added files permission");
174164
}
175165
}
176166

@@ -179,24 +169,19 @@ fn get_app_permissions(app_id: &str, is_flatpak: bool) -> Option<Vec<String>> {
179169
if line.contains("persist=") {
180170
let persist_part = line.split("persist=").nth(1).unwrap_or("");
181171
let persist_value = persist_part.split(';').next().unwrap_or("").trim();
182-
println!("[DEBUG] Found persist: '{}'", persist_value);
183172
if !persist_value.is_empty() {
184173
permissions.push("storage".to_string());
185174
has_storage = true;
186-
println!("[DEBUG] Added storage permission (persist)");
187175
}
188176
} else if line.contains("filesystems=") {
189177
if line.contains("home") || line.contains("host") || line.contains("xdg-download") {
190178
permissions.push("storage".to_string());
191179
has_storage = true;
192-
println!("[DEBUG] Added storage permission (filesystems with home/host/xdg-download)");
193180
}
194181
}
195182
}
196183
}
197184

198-
println!("[DEBUG] Final permissions for {}: {:?}", app_id, permissions);
199-
200185
if permissions.is_empty() {
201186
None
202187
} else {
@@ -252,21 +237,47 @@ struct SystemInfo {
252237
// Get system analytics data
253238
#[tauri::command]
254239
async fn get_app_permissions_batch(
255-
app: tauri::AppHandle,
240+
_app: tauri::AppHandle,
256241
app_ids: Vec<String>,
257242
) -> Result<std::collections::HashMap<String, Vec<String>>, String> {
258243
use std::collections::HashMap;
244+
use std::sync::{Arc, Mutex};
245+
use std::thread;
259246

260247
let is_flatpak = std::env::var("FLATPAK_ID").is_ok();
261-
let mut result = HashMap::new();
248+
let result = Arc::new(Mutex::new(HashMap::new()));
249+
250+
// Parallelize permission fetching using native threads
251+
// Limit concurrent threads to avoid overwhelming the system
252+
let chunk_size = 10; // Process 10 apps at a time
253+
let mut app_id_chunks: Vec<Vec<String>> = Vec::new();
254+
255+
for chunk in app_ids.chunks(chunk_size) {
256+
app_id_chunks.push(chunk.to_vec());
257+
}
262258

263-
for app_id in app_ids {
264-
if let Some(perms) = get_app_permissions(&app_id, is_flatpak) {
265-
result.insert(app_id, perms);
259+
for chunk in app_id_chunks.iter() {
260+
let mut handles = Vec::new();
261+
262+
for app_id in chunk {
263+
let result_clone = Arc::clone(&result);
264+
let app_id_clone = app_id.clone();
265+
let handle = thread::spawn(move || {
266+
if let Some(perms) = get_app_permissions(&app_id_clone, is_flatpak) {
267+
result_clone.lock().unwrap().insert(app_id_clone, perms);
268+
}
269+
});
270+
handles.push(handle);
271+
}
272+
273+
// Wait for this chunk to complete
274+
for handle in handles {
275+
let _ = handle.join();
266276
}
267277
}
268278

269-
Ok(result)
279+
let final_result = result.lock().unwrap().clone();
280+
Ok(final_result)
270281
}
271282

272283
#[tauri::command]
@@ -296,7 +307,6 @@ async fn get_system_analytics(
296307

297308
// Get system info
298309
let system_info = get_system_info().await?;
299-
300310
Ok(SystemAnalytics {
301311
disk_usage,
302312
flatpak_stats,

src/hooks/useInstalledApps.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,9 @@ export const useInstalledApps = () => {
4242
"get_installed_flatpaks",
4343
);
4444

45-
// Preload permissions for all apps in batch to avoid separate calls later
46-
let permissionsMap: Record<string, string[]> = {};
47-
if (response.apps.length > 0) {
48-
const appIds = response.apps.map((app) => app.app_id);
49-
try {
50-
permissionsMap = await invoke<Record<string, string[]>>(
51-
"get_app_permissions_batch",
52-
{ appIds },
53-
);
54-
} catch (permError) {
55-
console.error(
56-
"[useInstalledApps] Error loading permissions:",
57-
permError,
58-
);
59-
}
60-
}
61-
62-
// Convert apps from Rust format to TypeScript format with permissions
45+
// Convert apps from Rust format to TypeScript format
6346
// Generate unique instanceId for each app to handle duplicates
47+
// Permissions will be loaded on-demand when needed (e.g., in Analytics page)
6448
const installedAppsInfo: InstalledAppInfo[] = response.apps.map(
6549
(app) => ({
6650
instanceId: uuidv4(),
@@ -70,7 +54,7 @@ export const useInstalledApps = () => {
7054
summary: app.summary,
7155
developer: app.developer,
7256
installedSize: app.installed_size,
73-
permissions: permissionsMap[app.app_id] || [],
57+
permissions: [], // Loaded on-demand
7458
}),
7559
);
7660

src/hooks/useUpdateAll.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useState } from "react";
22
import type { InstalledAppInfo } from "../store/installedAppsStore";
3+
import { dbCacheManager } from "../utils/dbCache";
34
import {
45
updateFlatpakApp,
56
updateSystemFlatpaks,
@@ -58,6 +59,7 @@ export function useUpdateAll(onComplete?: () => void): UseUpdateAllReturn {
5859
});
5960

6061
let errorCount = 0;
62+
const successfullyUpdatedAppIds: string[] = [];
6163

6264
// ===== PHASE 1: Update user apps =====
6365
for (let i = 0; i < appsToUpdate.length; i++) {
@@ -95,6 +97,7 @@ export function useUpdateAll(onComplete?: () => void): UseUpdateAllReturn {
9597
"",
9698
`✓ ${app.name} actualizado exitosamente`,
9799
]);
100+
successfullyUpdatedAppIds.push(app.appId);
98101
} else {
99102
errorCount++;
100103
setUpdateAllOutput((prev) => [
@@ -201,6 +204,20 @@ export function useUpdateAll(onComplete?: () => void): UseUpdateAllReturn {
201204
}
202205
}
203206

207+
// Mark permissions as outdated for successfully updated apps
208+
if (successfullyUpdatedAppIds.length > 0) {
209+
try {
210+
await dbCacheManager.markPermissionsAsOutdatedBatch(
211+
successfullyUpdatedAppIds,
212+
);
213+
} catch (error) {
214+
console.error(
215+
"Error marking permissions as outdated after batch update:",
216+
error,
217+
);
218+
}
219+
}
220+
204221
// ===== PHASE 3: Complete =====
205222
setIsUpdatingAll(false);
206223
if (errorCount === 0) {

src/hooks/useUpdateApp.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useState } from "react";
2+
import { dbCacheManager } from "../utils/dbCache";
23
import { updateFlatpakApp } from "../utils/flatpakOperations";
34

45
interface UseUpdateAppReturn {
@@ -43,6 +44,13 @@ export function useUpdateApp(): UseUpdateAppReturn {
4344
"✓ Actualización completada exitosamente",
4445
]);
4546
setUpdateProgress(100);
47+
48+
// Mark permissions as outdated since the app was updated
49+
try {
50+
await dbCacheManager.markPermissionsAsOutdated(appId);
51+
} catch (error) {
52+
console.error("Error marking permissions as outdated:", error);
53+
}
4654
} else {
4755
setUpdateOutput((prev) => [
4856
...prev,

src/pages/analytics/Analytics.tsx

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from "react";
55
import { useTranslation } from "react-i18next";
66
import type { InstalledAppInfo } from "../../store/installedAppsStore";
77
import { useInstalledAppsStore } from "../../store/installedAppsStore";
8+
import { dbCacheManager } from "../../utils/dbCache";
89
import { DataCube } from "./components/DataCube";
910
import { SystemTerminal } from "./components/SystemTerminal";
1011

@@ -32,11 +33,15 @@ export const Analytics = ({ onBack }: AnalyticsProps) => {
3233
const [loading, setLoading] = useState(true);
3334
const [selectedApp, setSelectedApp] = useState<InstalledAppInfo | null>(null);
3435
const [permissionFilter, setPermissionFilter] = useState<string | null>(null);
36+
const [loadingPermissions, setLoadingPermissions] = useState(false);
3537

36-
// Get installed apps from store (permissions are preloaded by useInstalledApps)
38+
// Get installed apps from store
3739
const installedAppsInfo = useInstalledAppsStore(
3840
(state) => state.installedAppsInfo,
3941
);
42+
const setInstalledAppsInfo = useInstalledAppsStore(
43+
(state) => state.setInstalledAppsInfo,
44+
);
4045
const installedRuntimes = useInstalledAppsStore(
4146
(state) => state.installedRuntimes,
4247
);
@@ -54,10 +59,10 @@ export const Analytics = ({ onBack }: AnalyticsProps) => {
5459
const updateCount = Object.keys(availableUpdates).length;
5560

5661
const data = await invoke<SystemAnalytics>("get_system_analytics", {
57-
totalApps: installedAppsInfo.length || undefined,
58-
totalRuntimes: installedRuntimes.size || undefined,
59-
totalExtensions: extensionCount || undefined,
60-
appsWithUpdates: updateCount || undefined,
62+
totalApps: installedAppsInfo.length,
63+
totalRuntimes: installedRuntimes.size,
64+
totalExtensions: extensionCount,
65+
appsWithUpdates: updateCount,
6166
});
6267
setAnalytics(data);
6368
} catch (error) {
@@ -67,9 +72,93 @@ export const Analytics = ({ onBack }: AnalyticsProps) => {
6772
}
6873
}, [installedAppsInfo.length, installedRuntimes.size, availableUpdates]);
6974

75+
// Load permissions on-demand when Analytics page opens
76+
const loadPermissions = useCallback(async () => {
77+
// Check if permissions are already loaded in store
78+
const hasPermissions = installedAppsInfo.some(
79+
(app) => app.permissions && app.permissions.length > 0,
80+
);
81+
82+
if (hasPermissions || installedAppsInfo.length === 0) {
83+
return; // Already loaded or no apps
84+
}
85+
86+
try {
87+
setLoadingPermissions(true);
88+
89+
// Step 1: Try to get cached permissions from SQLite
90+
const cachedPermissions = await dbCacheManager.getCachedPermissionsBatch(
91+
installedAppsInfo.map((app) => ({
92+
appId: app.appId,
93+
version: app.version,
94+
})),
95+
);
96+
97+
// Step 2: Find apps that need permissions fetched
98+
const appsNeedingPermissions = installedAppsInfo.filter(
99+
(app) => !cachedPermissions[app.appId],
100+
);
101+
102+
let freshPermissions: Record<string, string[]> = {};
103+
104+
// Step 3: Fetch missing permissions from flatpak
105+
if (appsNeedingPermissions.length > 0) {
106+
const appIds = appsNeedingPermissions.map((app) => app.appId);
107+
freshPermissions = await invoke<Record<string, string[]>>(
108+
"get_app_permissions_batch",
109+
{ appIds },
110+
);
111+
112+
// Step 4: Cache the newly fetched permissions
113+
const permissionsToCache: Record<
114+
string,
115+
{ version: string; permissions: string[] }
116+
> = {};
117+
for (const app of appsNeedingPermissions) {
118+
if (freshPermissions[app.appId]) {
119+
permissionsToCache[app.appId] = {
120+
version: app.version,
121+
permissions: freshPermissions[app.appId],
122+
};
123+
}
124+
}
125+
await dbCacheManager.cachePermissionsBatch(permissionsToCache);
126+
}
127+
128+
// Step 5: Merge cached and fresh permissions
129+
const allPermissions = { ...cachedPermissions, ...freshPermissions };
130+
131+
// Step 6: Update apps with permissions
132+
const updatedApps = installedAppsInfo.map((app) => ({
133+
...app,
134+
permissions: allPermissions[app.appId] || [],
135+
}));
136+
setInstalledAppsInfo(updatedApps);
137+
138+
// Step 7: Clean old versions from cache (keep only current versions)
139+
// This prevents database from growing indefinitely
140+
try {
141+
await dbCacheManager.cleanOldPermissionsBatch(
142+
installedAppsInfo.map((app) => ({
143+
appId: app.appId,
144+
version: app.version,
145+
})),
146+
);
147+
} catch (error) {
148+
console.error("Error cleaning old permissions:", error);
149+
}
150+
} catch (error) {
151+
console.error("Error loading permissions:", error);
152+
} finally {
153+
setLoadingPermissions(false);
154+
}
155+
}, [installedAppsInfo, setInstalledAppsInfo]);
156+
70157
useEffect(() => {
71158
loadAnalytics();
72-
}, [loadAnalytics]);
159+
loadPermissions();
160+
// eslint-disable-next-line react-hooks/exhaustive-deps
161+
}, []); // Solo ejecutar una vez al montar el componente
73162

74163
return (
75164
<Box

src/pages/analytics/components/DataCube.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ const CubeScene = ({
204204
isGridView,
205205
permissionFilter,
206206
selectedApp,
207-
}: Omit<DataCubeProps, "loading"> & {
207+
}: Omit<DataCubeProps, "loading" | "onPermissionFilterChange"> & {
208208
isGridView: boolean;
209209
permissionFilter: PermissionFilter;
210210
selectedApp: InstalledAppInfo | null;
@@ -306,9 +306,6 @@ const CubeScene = ({
306306

307307
// Calcular filas necesarias para cada grupo
308308
const withPermRows = Math.ceil(appsWithPermission.length / maxCols);
309-
const _withoutPermRows = Math.ceil(
310-
appsWithoutPermission.length / maxCols,
311-
);
312309

313310
// Espacio vertical entre grupos (fila vacía)
314311
const groupSeparation = 2.0;

src/pages/myApps/MyApps.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export const MyApps = ({ onBack, onDeveloperSelect }: MyAppsProps) => {
186186
// Convert from Rust format to TypeScript format
187187
const installedAppsInfo: InstalledAppInfo[] = response.apps.map(
188188
(app) => ({
189+
instanceId: `${app.app_id}-${app.version}`,
189190
appId: app.app_id,
190191
name: app.name,
191192
version: app.version,

0 commit comments

Comments
 (0)