Skip to content
Open
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
74 changes: 74 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ On Linux, samply uses perf events. You can grant temporary access by running:
echo '-1' | sudo tee /proc/sys/kernel/perf_event_paranoid
```

On Windows, you can use `samply record -a` to record all processes. You'll usually also want to use some symbol servers, most importantly the Microsoft Symbol Server so that you can see symbols for Windows libraries. Here's a command which supports symbols for Windows, Firefox and Chrome:
On Windows, you can use `samply record -a` to record all processes. The Microsoft Symbol Server is pre-configured in the [config file](#configuration), so you get symbols for Windows libraries out of the box. You can add more symbol servers either in the config file or on the command line. Here's a command which adds symbols for Firefox and Chrome:

```
samply record -a --windows-symbol-server https://msdl.microsoft.com/download/symbols --breakpad-symbol-server https://symbols.mozilla.org/try/ --windows-symbol-server https://chromium-browser-symsrv.commondatastorage.googleapis.com
samply record -a --breakpad-symbol-server https://symbols.mozilla.org/try/ --windows-symbol-server https://chromium-browser-symsrv.commondatastorage.googleapis.com
```

## Installation
Expand Down Expand Up @@ -78,6 +78,7 @@ You can see which functions were running for how long. You can see flame graphs

All data is kept locally (on disk and in RAM) until you choose to upload your profile.


samply is a sampling profiler and collects stack traces, per thread, at some sampling interval (the default 1000Hz, i.e. 1ms). On macOS and Windows, both on- and off-cpu samples are collected (so you can see under which stack you were blocking on a lock, for example). On Linux, only on-cpu samples are collected at the moment.

On Linux, samply needs access to performance events system for unprivileged users. For this, you can either:
Expand Down Expand Up @@ -106,6 +107,12 @@ If you still get a `mmap failed` error (an `EPERM`), you might also need to incr
sudo sysctl kernel.perf_event_mlock_kb=2048
```

### Symbols and configuration

Symbol servers can be configured in the config file at `~/.config/samply/config.toml` (`%APPDATA%\samply\config.toml` on Windows). You can also specify the maximum size of the symbol cache and of the profile store.

By default, the profiles produces by `samply record` and `samply import` are stored in `~/.local/share/samply/profiles/` (`%LOCALAPPDATA%\samply\profiles\` on Windows).

## Examples

Here's a profile from `samply record rustup check`: https://share.firefox.dev/3hteKZZ
Expand Down
13 changes: 12 additions & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

## Unreleased - ReleaseDate

### Breaking changes

- All platforms: `samply record` and `samply import` no longer write `profile.jslb.gz` into the current directory by default. Profiles are now stored in the per-user profile store (`~/.local/share/samply/profiles` on macOS and Linux, `%LOCALAPPDATA%\samply\profiles` on Windows), and the path of the saved profile is printed. Pass `-o <path>` to write the profile somewhere else.

### Features

- All platforms: Add a config file at `~/.config/samply/config.toml` (`%APPDATA%\samply\config.toml` on Windows). It configures symbol servers, symbol directories, and the eviction limits of the profile store and the symbol cache. A commented template is written on first run. Use `--config <path>` or `SAMPLY_CONFIG` to point samply at a different file.
- All platforms: Old profiles in the profile store are deleted automatically, by default after 30 days or once the store exceeds 5 GB. Profiles created or opened within the last day are never deleted.
- All platforms: The symbol cache limits (previously fixed at 10 GB / 2 weeks) can now be configured, and symbols used within the last day are no longer deleted at startup.
- Windows: The Microsoft Symbol Server is enabled by default in the generated config file, so system library symbols work without `--windows-symbol-server`.

## 0.13.1 - 2025-02-01

## 0.13.0 - 2025-02-01
Expand All @@ -20,7 +31,7 @@ And thanks to the authors of the https://github.com/n4r1b/ferrisetw crate; sampl

Known issues:

- By default, you won't get Windows symbols, but you can use `samply record --windows-symbol-server https://msdl.microsoft.com/download/symbols` to fix this - this will download symbols for Windows system libraries and kernel stacks from Microsoft's server. I'm planning to add a config file for samply so that symbol servers can be configured more permanently, but it doesn't exist yet.
- By default, you won't get Windows symbols, but you can use `samply record --windows-symbol-server https://msdl.microsoft.com/download/symbols` to fix this - this will download symbols for Windows system libraries and kernel stacks from Microsoft's server. (Fixed in the next release: the Microsoft Symbol Server is now in the config file by default.)
- Missing symbols for precompiled .NET code: This is [getsentry/pdb#153](https://github.com/getsentry/pdb/issues/153), which has a potential patch in [getsentry/pdb#154](https://github.com/getsentry/pdb/pull/154).
- CoreCLR support could be better - some of it isn't working correctly any more (see [#483](https://github.com/mstange/samply/issues/483))

Expand Down
12 changes: 12 additions & 0 deletions samply-quota-manager/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
# samply-quota-manager

Limits the total size of a directory by deleting least-recently-used files.

`QuotaManager` keeps an inventory of the files in a managed directory in a
sqlite database. You tell it about created and accessed files, and it enforces
three optional limits when asked to evict:

- `set_max_age`: delete files which haven't been accessed for this long.
- `set_max_total_size`: delete least-recently-used files until the total size
is below the limit.
- `set_min_age`: never delete files which were accessed within this time span,
even if that means the size limit is exceeded.

40 changes: 32 additions & 8 deletions samply-quota-manager/src/file_inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,15 @@ impl FileInventory {
/// Returns a list of file paths. Deleting all the listed files will reduce
/// the total size of the managed directory below `max_size_bytes`, assuming
/// that the information stored in the DB is complete and accurate.
pub fn get_files_to_delete_to_enforce_max_size(&self, max_size_bytes: u64) -> Vec<FileInfo> {
///
/// Files which were last accessed at or after `protect_accessed_after` are
/// never included in the list. If protected files alone exceed the limit,
/// the returned list is not sufficient to get below `max_size_bytes`.
pub fn get_files_to_delete_to_enforce_max_size(
&self,
max_size_bytes: u64,
protect_accessed_after: Option<SystemTime>,
) -> Vec<FileInfo> {
let total_size = self.total_size_in_bytes();
if total_size <= max_size_bytes {
// Nothing needs to be deleted.
Expand All @@ -282,11 +290,15 @@ impl FileInventory {

let mut stmt = self
.db_connection
.prepare_cached("SELECT Path, Size, CreationTime, LastAccessTime FROM files ORDER BY LastAccessTime ASC")
.prepare_cached("SELECT Path, Size, CreationTime, LastAccessTime FROM files WHERE LastAccessTime < ?1 ORDER BY LastAccessTime ASC")
.unwrap();

let candidate_bound = match protect_accessed_after {
Some(protect_accessed_after) => SqliteTime::from(protect_accessed_after).0,
None => i64::MAX,
};
let files = stmt
.query_map([], |row| self.file_info_from_row(row))
.query_map([candidate_bound], |row| self.file_info_from_row(row))
.unwrap()
.filter_map(Result::ok);

Expand Down Expand Up @@ -329,16 +341,28 @@ impl FileInventory {

// Delete the largest files first.
files_to_delete.sort_unstable_by_key(|file_info| {
let size = i32::try_from(file_info.size_in_bytes).unwrap();
let negative_size = size.checked_neg().unwrap();
(negative_size, file_info.last_access_time)
(
std::cmp::Reverse(file_info.size_in_bytes),
file_info.last_access_time,
)
});
files_to_delete
}

/// Returns a list of file paths, listing all the files whose last access time (as
/// stored by the inventory) is older than `max_age_seconds`.
pub fn get_files_last_accessed_before(&self, cutoff_time: SystemTime) -> Vec<FileInfo> {
/// stored by the inventory) is before `cutoff_time`.
///
/// Files which were last accessed at or after `protect_accessed_after` are
/// never included in the list.
pub fn get_files_last_accessed_before(
&self,
cutoff_time: SystemTime,
protect_accessed_after: Option<SystemTime>,
) -> Vec<FileInfo> {
let cutoff_time = match protect_accessed_after {
Some(protect_accessed_after) => cutoff_time.min(protect_accessed_after),
None => cutoff_time,
};
let mut stmt = self
.db_connection
.prepare_cached("SELECT Path, Size, CreationTime, LastAccessTime FROM files WHERE LastAccessTime < ?1")
Expand Down
23 changes: 20 additions & 3 deletions samply-quota-manager/src/quota_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl Clone for QuotaManagerNotifier {
struct EvictionSettings {
max_size_bytes: Option<u64>,
max_age_seconds: Option<u64>,
min_age_seconds: Option<u64>,
}

impl QuotaManager {
Expand Down Expand Up @@ -124,6 +125,15 @@ impl QuotaManager {
self.settings.lock().unwrap().max_age_seconds = max_age_seconds;
}

/// Change the minimum age of tracked files in the managed directory,
/// in seconds. Files which were accessed within this time span are never
/// deleted, not even to enforce the maximum total size.
///
/// Respected during the next eviction.
pub fn set_min_age(&self, min_age_seconds: Option<u64>) {
self.settings.lock().unwrap().min_age_seconds = min_age_seconds;
}

/// Returns the current total size of the managed directory, in bytes.
///
/// The return value is only correct if the information in the database
Expand Down Expand Up @@ -246,16 +256,22 @@ impl QuotaManagerEvictionThread {
ByteSize(total_size_before).display().si()
);

// Files accessed after `protect_accessed_after` are never deleted.
let now = SystemTime::now();
let protect_accessed_after = settings
.min_age_seconds
.map(|min_age_seconds| now - Duration::from_secs(min_age_seconds));

// Enforce max age first, and size limit second.
// We know that files older than the max age need to be deleted anyway.
// This may already free up some space. Then we can delete more files in
// order to enforce the size limit.

let files_to_delete_for_enforcing_max_age = match settings.max_age_seconds {
Some(max_age_seconds) => {
let cutoff_time = SystemTime::now() - Duration::from_secs(max_age_seconds);
let cutoff_time = now - Duration::from_secs(max_age_seconds);
let inventory = self.inventory.lock().unwrap();
inventory.get_files_last_accessed_before(cutoff_time)
inventory.get_files_last_accessed_before(cutoff_time, protect_accessed_after)
}
None => vec![],
};
Expand All @@ -273,7 +289,8 @@ impl QuotaManagerEvictionThread {
let files_to_delete_for_enforcing_max_size = match settings.max_size_bytes {
Some(max_size_bytes) => {
let inventory = self.inventory.lock().unwrap();
inventory.get_files_to_delete_to_enforce_max_size(max_size_bytes)
inventory
.get_files_to_delete_to_enforce_max_size(max_size_bytes, protect_accessed_after)
}
None => vec![],
};
Expand Down
Loading
Loading