-
Notifications
You must be signed in to change notification settings - Fork 35
Enhance image optimization with compression and CPU affinity support #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
28535a7
feat(disk): preserve compression when optimizing images
lateautumn233 fc83732
feat(disk): add compression option to convert dialog
lateautumn233 1ffa0f4
feat(cpu): add CPU affinity support for disk operations
lateautumn233 5b4d041
refactor(disk): address review feedback on CPU affinity and image utils
lateautumn233 2a3aead
fix(cpu): format CPU frequency with decimal (SI) units
lateautumn233 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
219 changes: 219 additions & 0 deletions
219
app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| package cn.classfun.droidvm.lib.utils; | ||
|
|
||
| import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; | ||
|
|
||
| import android.util.Log; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
|
|
||
| import java.io.File; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.TreeSet; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * Host CPU topology helper: enumerates cores, reads each core's max frequency | ||
| * from sysfs and groups them into frequency tiers so callers can tell the | ||
| * little cluster apart from the big/prime clusters (for CPU-affinity binding). | ||
| * Reads are best-effort; a core whose frequency cannot be read is reported with | ||
| * {@code maxFreqKHz == 0} and treated as tier 0. | ||
| */ | ||
| public final class CpuUtils { | ||
| private static final String TAG = "CpuUtils"; | ||
| private static final String CPU_ROOT = "/sys/devices/system/cpu"; | ||
| private static final Pattern CPU_DIR = Pattern.compile("cpu\\d+"); | ||
|
|
||
| private CpuUtils() { | ||
| } | ||
|
|
||
| /** A single host CPU core and its cluster classification. */ | ||
| public static final class CpuCore { | ||
| public final int index; | ||
| public final long maxFreqKHz; // 0 when unknown | ||
| public final int tier; // 0 = lowest-freq cluster, ascending | ||
| public final boolean big; // true when not in the lowest-freq cluster | ||
|
|
||
| CpuCore(int index, long maxFreqKHz, int tier, boolean big) { | ||
| this.index = index; | ||
| this.maxFreqKHz = maxFreqKHz; | ||
| this.tier = tier; | ||
| this.big = big; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Enumerate host cores sorted by index, each tagged with its frequency tier. | ||
| * Never returns null; falls back to {@link Runtime#availableProcessors()} | ||
| * with unknown frequencies if sysfs cannot be read. | ||
| */ | ||
| @NonNull | ||
| public static List<CpuCore> getCores() { | ||
| var indices = listCoreIndices(); | ||
| var freqs = new long[indices.size()]; | ||
| var distinct = new TreeSet<Long>(); | ||
| for (int i = 0; i < indices.size(); i++) { | ||
| freqs[i] = readMaxFreqKHz(indices.get(i)); | ||
| if (freqs[i] > 0) distinct.add(freqs[i]); | ||
| } | ||
| // Ascending tier index per distinct frequency; unknown (0) stays tier 0. | ||
| var tierOf = new ArrayList<>(distinct); | ||
| var cores = new ArrayList<CpuCore>(indices.size()); | ||
| for (int i = 0; i < indices.size(); i++) { | ||
| int tier = freqs[i] > 0 ? tierOf.indexOf(freqs[i]) : 0; | ||
| cores.add(new CpuCore(indices.get(i), freqs[i], tier, tier > 0)); | ||
| } | ||
| return cores; | ||
| } | ||
|
|
||
| /** Number of distinct frequency clusters (1 when frequencies are unknown). */ | ||
| public static int tierCount(@NonNull List<CpuCore> cores) { | ||
| int max = 0; | ||
| for (var c : cores) max = Math.max(max, c.tier); | ||
| return max + 1; | ||
| } | ||
|
|
||
| /** | ||
| * Default selection for "filter out the little cores": every core that is | ||
| * not in the lowest-frequency cluster. When there is only a single cluster | ||
| * (or detection failed) all cores are returned, since filtering is moot. | ||
| */ | ||
| @NonNull | ||
| public static String defaultBigCoresCsv() { | ||
| var cores = getCores(); | ||
| boolean single = tierCount(cores) <= 1; | ||
| var sb = new StringBuilder(); | ||
| for (var c : cores) { | ||
| if (single || c.big) { | ||
| if (sb.length() > 0) sb.append(','); | ||
| sb.append(c.index); | ||
| } | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| // Frequency uses decimal (SI) steps of 1000, unlike SizeUtils' binary units. | ||
| private static final String[] FREQ_UNITS = {"Hz", "kHz", "MHz", "GHz", "THz"}; | ||
|
|
||
| /** Format a KHz frequency as e.g. "2.60 GHz"; empty string when unknown. */ | ||
| @NonNull | ||
| public static String formatFreq(long khz) { | ||
| if (khz <= 0) return ""; | ||
| double value = khz * 1000.0; | ||
| int tier = 0; | ||
| while (value >= 1000.0 && tier < FREQ_UNITS.length - 1) { | ||
| value /= 1000.0; | ||
| tier++; | ||
| } | ||
| return fmt("%.2f %s", value, FREQ_UNITS[tier]); | ||
| } | ||
|
|
||
| /** | ||
| * Collapse a core-index CSV into a compact range string, e.g. | ||
| * "4,5,6,7" -> "4-7", "0,4,5,6,7" -> "0,4-7". Returns "" for empty input. | ||
| */ | ||
| @NonNull | ||
| public static String compactRanges(@NonNull String csv) { | ||
| var nums = parseCsv(csv); | ||
| if (nums.isEmpty()) return ""; | ||
| var sb = new StringBuilder(); | ||
| int start = nums.get(0), prev = start; | ||
| for (int i = 1; i <= nums.size(); i++) { | ||
| int cur = i < nums.size() ? nums.get(i) : Integer.MIN_VALUE; | ||
| if (cur == prev + 1) { | ||
| prev = cur; | ||
| continue; | ||
| } | ||
| if (sb.length() > 0) sb.append(','); | ||
| if (start == prev) sb.append(start); | ||
| else sb.append(start).append('-').append(prev); | ||
| start = prev = cur; | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Convert a core-index CSV into the hex CPU mask that toybox {@code taskset} | ||
| * expects (no "0x" prefix), e.g. "4,5,6,7" -> "f0", "0,1" -> "3". Returns | ||
| * "" for empty input. Bit N set means core N is allowed. | ||
| */ | ||
| @NonNull | ||
| public static String coresCsvToHexMask(@NonNull String csv) { | ||
| var nums = parseCsv(csv); | ||
| if (nums.isEmpty()) return ""; | ||
| var mask = java.math.BigInteger.ZERO; | ||
| for (int idx : nums) { | ||
| if (idx >= 0) mask = mask.setBit(idx); | ||
| } | ||
| return mask.signum() == 0 ? "" : mask.toString(16); | ||
| } | ||
|
|
||
| @NonNull | ||
| private static List<Integer> parseCsv(@NonNull String csv) { | ||
| var out = new ArrayList<Integer>(); | ||
| if (csv.isEmpty()) return out; | ||
| for (var part : csv.split(",")) { | ||
| part = part.trim(); | ||
| if (part.isEmpty()) continue; | ||
| try { | ||
| out.add(Integer.parseInt(part)); | ||
| } catch (NumberFormatException ignored) { | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| @NonNull | ||
| private static List<Integer> listCoreIndices() { | ||
| var out = new ArrayList<Integer>(); | ||
| var root = new File(CPU_ROOT); | ||
| var dirs = root.listFiles(); | ||
| if (dirs != null) { | ||
| for (var d : dirs) { | ||
| if (!d.isDirectory() || !CPU_DIR.matcher(d.getName()).matches()) continue; | ||
| try { | ||
| out.add(Integer.parseInt(d.getName().substring(3))); | ||
| } catch (NumberFormatException ignored) { | ||
| } | ||
| } | ||
| } | ||
| if (out.isEmpty()) { | ||
| // sysfs unreadable -- fall back to the runtime-reported count. | ||
| int n = Math.max(1, Runtime.getRuntime().availableProcessors()); | ||
| for (int i = 0; i < n; i++) out.add(i); | ||
| } else { | ||
| out.sort(Integer::compareTo); | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| private static long readMaxFreqKHz(int index) { | ||
| // cpuinfo_max_freq is the hardware ceiling; scaling_max_freq is the | ||
| // policy ceiling (usually equal). Try the direct read first, then a | ||
| // root-backed read, before giving up on this core. | ||
| long v = tryReadFreq(fmt("%s/cpu%d/cpufreq/cpuinfo_max_freq", CPU_ROOT, index)); | ||
| if (v > 0) return v; | ||
| return tryReadFreq(fmt("%s/cpu%d/cpufreq/scaling_max_freq", CPU_ROOT, index)); | ||
| } | ||
|
|
||
| private static long tryReadFreq(@NonNull String path) { | ||
| String raw = null; | ||
| try { | ||
| raw = FileUtils.readFile(path); | ||
| } catch (Exception directFailed) { | ||
| try { | ||
| raw = FileUtils.shellReadFile(path); | ||
| } catch (Exception rootFailed) { | ||
| Log.d(TAG, fmt("freq read failed: %s", path)); | ||
| } | ||
| } | ||
| if (raw == null) return 0; | ||
| raw = raw.trim(); | ||
| if (raw.isEmpty()) return 0; | ||
| try { | ||
| return Long.parseLong(raw); | ||
| } catch (NumberFormatException e) { | ||
| return 0; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,4 +28,37 @@ public static JSONObject getImageInfo(String path) throws JSONException { | |
| } | ||
| return new JSONObject(result.getOutString()); | ||
| } | ||
|
|
||
| @NonNull | ||
| public static JSONObject getImageCheck(String path) throws JSONException { | ||
| var result = runListQuiet( | ||
| getPrebuiltBinaryPath("qemu-img"), | ||
| "check", "--output=json", path | ||
| ); | ||
| // qemu-img check still writes its JSON report on non-zero exits | ||
| // (leaks/corruptions), so parse whatever it produced rather than | ||
| // gating on isSuccess(); a missing/garbage body throws. | ||
| var out = result.getOutString(); | ||
| if (out == null || out.isEmpty()) | ||
| throw new JSONException("qemu-img check produced no output"); | ||
| return new JSONObject(out); | ||
| } | ||
|
|
||
| /** | ||
| * Whether {@code path} actually stores compressed clusters, per | ||
| * {@code qemu-img check}'s {@code compressed-clusters} count. This is the | ||
| * real signal: the qcow2 header's {@code compression-type} reads "zlib" for | ||
| * every v3 image (even ones with no compressed data), so it cannot tell an | ||
| * uncompressed disk from a compressed one. Unlike lbx, qemu-img reads both | ||
| * zlib and zstd images, so a zstd disk is detected too. Any detection | ||
| * failure (raw images reject {@code check}, etc.) returns {@code false} -- | ||
| * an undetectable image is treated as uncompressed. | ||
| */ | ||
| public static boolean hasCompressedClusters(String path) { | ||
| try { | ||
| return getImageCheck(path).optLong("compressed-clusters", 0) > 0; | ||
| } catch (Exception e) { | ||
| return false; | ||
| } | ||
| } | ||
|
Comment on lines
+57
to
+63
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 拆分一个 |
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
复用SizeUtils.findFloatUnit