Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions app/src/main/java/cn/classfun/droidvm/lib/utils/CpuUtils.java
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]);
}
Comment on lines +100 to +109

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复用SizeUtils.findFloatUnit


/**
* 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;
}
}
}
33 changes: 33 additions & 0 deletions app/src/main/java/cn/classfun/droidvm/lib/utils/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

拆分一个getImageCheck函数,参考getImageInfo

}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public void tryOptimize(@NonNull DiskConfig config) {
try {
var info = ImageUtils.getImageInfo(config.getFullPath());
obj.put("action", "convert");
obj.put("keep_compress", true); // preserve compression when optimizing
obj.put("format", info.getString("format"));
if (info.has("backing-filename"))
obj.put("backing_path", info.getString("backing-filename"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import static cn.classfun.droidvm.lib.utils.ThreadUtils.runOnPool;
import static cn.classfun.droidvm.ui.disk.operation.DiskOperationActivity.startConvert;

import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static cn.classfun.droidvm.lib.store.disk.DiskConfig.supportsCompress;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
Expand All @@ -23,6 +27,7 @@
import cn.classfun.droidvm.R;
import cn.classfun.droidvm.lib.store.disk.DiskConfig;
import cn.classfun.droidvm.lib.utils.ImageUtils;
import cn.classfun.droidvm.ui.disk.create.DiskCompress;
import cn.classfun.droidvm.ui.disk.create.DiskFormat;
import cn.classfun.droidvm.ui.widgets.row.ChooseRowWidget;
import cn.classfun.droidvm.ui.widgets.row.TextInputRowWidget;
Expand All @@ -34,6 +39,7 @@ public final class DiskSetFormatDialog {
private final DiskConfig config;
private TextInputRowWidget inputName;
private ChooseRowWidget chooseFormat;
private ChooseRowWidget chooseCompress;
private TextView tvCurrentFormat;
private DiskFormat currentFormat = null;

Expand All @@ -47,14 +53,18 @@ public void show() {
tvCurrentFormat = view.findViewById(R.id.tv_current_format);
inputName = view.findViewById(R.id.input_name);
chooseFormat = view.findViewById(R.id.choose_format);
chooseCompress = view.findViewById(R.id.choose_compress);
chooseFormat.setItems(DiskFormat.class);
chooseCompress.configure(DiskCompress.class, DiskCompress.DEFLATE);
inputName.setText(config.getName());
tvCurrentFormat.setText(R.string.disk_set_format_loading);
runOnPool(this::loadImageInfo);
chooseFormat.setOnValueChangedListener(() -> {
DiskFormat fmt = chooseFormat.getSelectedItem();
updateFileName(inputName, config.getName(), fmt.getExt());
updateCompressVisibility(fmt);
});
updateCompressVisibility(chooseFormat.getSelectedItem());
var dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.disk_set_format_title)
.setView(view)
Expand All @@ -70,16 +80,25 @@ public void show() {
}
inputName.setError(null);
DiskFormat format = chooseFormat.getSelectedItem();
if (format.equals(currentFormat)) {
// Same format is still a valid conversion when it carries a
// compression choice (qcow2): the user may only be re-compressing.
if (format.equals(currentFormat) && !supportsCompress(format)) {
Toast.makeText(context, R.string.disk_set_format_error_same, LENGTH_LONG).show();
return;
}
dialog.dismiss();
var output = pathJoin(config.item.optString("folder", ""), name);
startConvert(context, config.getId(), format.name().toLowerCase(), output);
var compress = supportsCompress(format)
? chooseCompress.<DiskCompress>getSelectedItem().name().toLowerCase()
: "none";
startConvert(context, config.getId(), format.name().toLowerCase(), output, compress);
});
}

private void updateCompressVisibility(DiskFormat fmt) {
chooseCompress.setVisibility(supportsCompress(fmt) ? VISIBLE : GONE);
}

private void loadImageInfo() {
try {
currentFormat = null;
Expand All @@ -100,6 +119,7 @@ private void loadImageInfo() {
currentFormat = chooseFormat.getSelectedItem();
}
updateFileName(inputName, config.getName(), currentFormat.getExt());
updateCompressVisibility(chooseFormat.getSelectedItem());
});
}

Expand Down
Loading
Loading