From e145e5c2076e583e5afa98411147e9cdcabfbdf9 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Thu, 15 Feb 2024 16:30:54 +0100 Subject: [PATCH] Add a basic `proguard` workspace crate (#1371) For now symbolicator-proguard includes only a ProguardService that can download proguard files from Sentry sources and turn them into ProguardMapper structs. ----------------------------- Co-authored-by: Yagiz Nizipli Co-authored-by: Sebastian Zivota --- Cargo.lock | 24 +++ crates/symbolicator-proguard/Cargo.toml | 21 +++ crates/symbolicator-proguard/src/interface.rs | 1 + crates/symbolicator-proguard/src/lib.rs | 4 + crates/symbolicator-proguard/src/service.rs | 150 ++++++++++++++++++ .../tests/integration/main.rs | 4 + .../tests/integration/proguard.rs | 54 +++++++ .../tests/integration/utils.rs | 35 ++++ .../src/caches/versions.rs | 8 + .../src/caching/cleanup.rs | 2 + .../src/caching/config.rs | 2 + .../symbolicator-service/src/caching/mod.rs | 10 +- .../src/download/sentry.rs | 2 + crates/symbolicator-sources/src/filetype.rs | 4 + crates/symbolicator-sources/src/paths.rs | 5 + crates/symbolicator-stress/Cargo.toml | 1 + crates/symbolicator-test/src/lib.rs | 43 ++++- crates/symbolicator/Cargo.toml | 1 + crates/symbolicator/src/service.rs | 2 + tests/fixtures/proguard/01/proguard.txt | 35 ++++ 20 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 crates/symbolicator-proguard/Cargo.toml create mode 100644 crates/symbolicator-proguard/src/interface.rs create mode 100644 crates/symbolicator-proguard/src/lib.rs create mode 100644 crates/symbolicator-proguard/src/service.rs create mode 100644 crates/symbolicator-proguard/tests/integration/main.rs create mode 100644 crates/symbolicator-proguard/tests/integration/proguard.rs create mode 100644 crates/symbolicator-proguard/tests/integration/utils.rs create mode 100644 tests/fixtures/proguard/01/proguard.txt diff --git a/Cargo.lock b/Cargo.lock index 22f82becf..a6b39b5bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3075,6 +3075,12 @@ dependencies = [ "hex", ] +[[package]] +name = "proguard" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02edf5746919e655cfec7b1accbfc889d76b80e5cf72ba4a4da8024b2c2ee1a6" + [[package]] name = "psm" version = "0.1.21" @@ -4396,6 +4402,7 @@ dependencies = [ "symbolicator-crash", "symbolicator-js", "symbolicator-native", + "symbolicator-proguard", "symbolicator-service", "symbolicator-sources", "symbolicator-test", @@ -4478,6 +4485,22 @@ dependencies = [ "url", ] +[[package]] +name = "symbolicator-proguard" +version = "24.1.2" +dependencies = [ + "futures", + "proguard", + "serde", + "serde_json", + "symbolic", + "symbolicator-service", + "symbolicator-sources", + "symbolicator-test", + "tempfile", + "tokio", +] + [[package]] name = "symbolicator-service" version = "24.1.2" @@ -4553,6 +4576,7 @@ dependencies = [ "serde_yaml", "symbolicator-js", "symbolicator-native", + "symbolicator-proguard", "symbolicator-service", "symbolicator-test", "tempfile", diff --git a/crates/symbolicator-proguard/Cargo.toml b/crates/symbolicator-proguard/Cargo.toml new file mode 100644 index 000000000..1a0189dd8 --- /dev/null +++ b/crates/symbolicator-proguard/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "symbolicator-proguard" +publish = false +version = "24.1.2" +authors = ["Sentry "] +edition = "2021" +license = "MIT" + +[dependencies] +futures = "0.3.12" +proguard = "5.4.0" +serde = { version = "1.0.137", features = ["derive", "rc"] } +serde_json = "1.0.81" +symbolic = "12.8.0" +symbolicator-service = { path = "../symbolicator-service" } +symbolicator-sources = { path = "../symbolicator-sources" } +tempfile = "3.10.0" + +[dev-dependencies] +symbolicator-test = { path = "../symbolicator-test" } +tokio = { version = "1.24.2", features = ["rt", "macros", "fs"] } diff --git a/crates/symbolicator-proguard/src/interface.rs b/crates/symbolicator-proguard/src/interface.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/symbolicator-proguard/src/interface.rs @@ -0,0 +1 @@ + diff --git a/crates/symbolicator-proguard/src/lib.rs b/crates/symbolicator-proguard/src/lib.rs new file mode 100644 index 000000000..4e12db88c --- /dev/null +++ b/crates/symbolicator-proguard/src/lib.rs @@ -0,0 +1,4 @@ +pub mod interface; +mod service; + +pub use service::ProguardService; diff --git a/crates/symbolicator-proguard/src/service.rs b/crates/symbolicator-proguard/src/service.rs new file mode 100644 index 000000000..3413bd147 --- /dev/null +++ b/crates/symbolicator-proguard/src/service.rs @@ -0,0 +1,150 @@ +use std::sync::Arc; + +use futures::future::BoxFuture; +use symbolic::common::{AsSelf, ByteView, DebugId, SelfCell}; +use symbolicator_service::caches::versions::PROGUARD_CACHE_VERSIONS; +use symbolicator_service::caching::{ + CacheEntry, CacheError, CacheItemRequest, CacheKey, CacheVersions, Cacher, +}; +use symbolicator_service::download::{fetch_file, DownloadService}; +use symbolicator_service::services::SharedServices; +use symbolicator_service::types::Scope; +use symbolicator_sources::{FileType, ObjectId, RemoteFile, SourceConfig}; +use tempfile::NamedTempFile; + +#[derive(Debug, Clone)] +pub struct ProguardService { + pub(crate) download_svc: Arc, + pub(crate) cache: Arc>, +} + +impl ProguardService { + pub fn new(services: &SharedServices) -> Self { + let caches = &services.caches; + let shared_cache = services.shared_cache.clone(); + let download_svc = services.download_svc.clone(); + + let cache = Arc::new(Cacher::new(caches.proguard.clone(), shared_cache)); + + Self { + download_svc, + cache, + } + } + + async fn find_proguard_file( + &self, + sources: &[SourceConfig], + identifier: &ObjectId, + ) -> Option { + let file_ids = self + .download_svc + .list_files(sources, &[FileType::Proguard], identifier) + .await; + + file_ids.into_iter().next() + } + + /// Retrieves the given [`RemoteFile`] from cache, or fetches it and persists it according + /// to the provided [`Scope`]. + /// It is possible to avoid using the shared cache using the `use_shared_cache` parameter. + pub async fn fetch_file(&self, scope: &Scope, file: RemoteFile) -> CacheEntry { + let cache_key = CacheKey::from_scoped_file(scope, &file); + + let request = FetchProguard { + file, + download_svc: Arc::clone(&self.download_svc), + }; + + self.cache.compute_memoized(request, cache_key).await + } + + pub async fn download_proguard_file( + &self, + sources: &[SourceConfig], + scope: &Scope, + debug_id: DebugId, + ) -> CacheEntry { + let identifier = ObjectId { + debug_id: Some(debug_id), + ..Default::default() + }; + + let remote_file = self + .find_proguard_file(sources, &identifier) + .await + .ok_or(CacheError::NotFound)?; + + self.fetch_file(scope, remote_file).await + } +} + +struct ProguardInner<'a> { + // TODO: actually use it + #[allow(unused)] + mapping: proguard::ProguardMapping<'a>, + // TODO: actually use it + #[allow(unused)] + mapper: proguard::ProguardMapper<'a>, +} + +impl<'slf, 'a: 'slf> AsSelf<'slf> for ProguardInner<'a> { + type Ref = ProguardInner<'slf>; + + fn as_self(&'slf self) -> &Self::Ref { + self + } +} + +#[derive(Clone)] +pub struct ProguardMapper { + // TODO: actually use it + #[allow(unused)] + inner: Arc, ProguardInner<'static>>>, +} + +#[derive(Clone, Debug)] +pub struct FetchProguard { + file: RemoteFile, + download_svc: Arc, +} + +impl CacheItemRequest for FetchProguard { + type Item = ProguardMapper; + + const VERSIONS: CacheVersions = PROGUARD_CACHE_VERSIONS; + + fn compute<'a>(&'a self, temp_file: &'a mut NamedTempFile) -> BoxFuture<'a, CacheEntry> { + let fut = async { + fetch_file(self.download_svc.clone(), self.file.clone(), temp_file).await?; + + let view = ByteView::map_file_ref(temp_file.as_file())?; + + let mapping = proguard::ProguardMapping::new(&view); + if mapping.is_valid() { + Ok(()) + } else { + Err(CacheError::Malformed( + "The file is not a valid ProGuard file".into(), + )) + } + }; + Box::pin(fut) + } + + fn load(&self, byteview: ByteView<'static>) -> CacheEntry { + let inner = SelfCell::new(byteview, |data| { + let mapping = proguard::ProguardMapping::new(unsafe { &*data }); + let mapper = proguard::ProguardMapper::new(mapping.clone()); + ProguardInner { mapping, mapper } + }); + + Ok(ProguardMapper { + inner: Arc::new(inner), + }) + } + + fn use_shared_cache(&self) -> bool { + false + } +} diff --git a/crates/symbolicator-proguard/tests/integration/main.rs b/crates/symbolicator-proguard/tests/integration/main.rs new file mode 100644 index 000000000..2a5c9098e --- /dev/null +++ b/crates/symbolicator-proguard/tests/integration/main.rs @@ -0,0 +1,4 @@ +mod proguard; +mod utils; + +pub use utils::*; diff --git a/crates/symbolicator-proguard/tests/integration/proguard.rs b/crates/symbolicator-proguard/tests/integration/proguard.rs new file mode 100644 index 000000000..b2aceeac3 --- /dev/null +++ b/crates/symbolicator-proguard/tests/integration/proguard.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; +use std::{collections::HashMap, str::FromStr}; + +use serde_json::json; +use symbolic::common::DebugId; +use symbolicator_service::types::Scope; +use symbolicator_sources::{SentrySourceConfig, SourceConfig}; + +use crate::setup_service; + +fn proguard_server( + fixtures_dir: &str, + lookup: L, +) -> (symbolicator_test::Server, SentrySourceConfig) +where + L: Fn(&str, &HashMap) -> serde_json::Value + Clone + Send + 'static, +{ + let fixtures_dir = symbolicator_test::fixture(format!("proguard/{fixtures_dir}")); + symbolicator_test::sentry_server(fixtures_dir, lookup) +} + +#[tokio::test] +async fn test_download_proguard_file() { + symbolicator_test::setup(); + let (symbolication, _cache_dir) = setup_service(|_| ()); + let (_srv, source) = proguard_server("01", |_url, _query| { + json!([{ + "id":"proguard.txt", + "uuid":"246fb328-fc4e-406a-87ff-fc35f6149d8f", + "debugId":"246fb328-fc4e-406a-87ff-fc35f6149d8f", + "codeId":null, + "cpuName":"any", + "objectName":"proguard-mapping", + "symbolType":"proguard", + "headers": { + "Content-Type":"text/x-proguard+plain" + }, + "size":3619, + "sha1":"deba83e73fd18210a830db372a0e0a2f2293a989", + "dateCreated":"2024-02-14T10:49:38.770116Z", + "data":{ + "features":["mapping"] + } + }]) + }); + + let source = SourceConfig::Sentry(Arc::new(source)); + let debug_id = DebugId::from_str("246fb328-fc4e-406a-87ff-fc35f6149d8f").unwrap(); + + assert!(symbolication + .download_proguard_file(&[source], &Scope::Global, debug_id) + .await + .is_ok()); +} diff --git a/crates/symbolicator-proguard/tests/integration/utils.rs b/crates/symbolicator-proguard/tests/integration/utils.rs new file mode 100644 index 000000000..c9342d9ac --- /dev/null +++ b/crates/symbolicator-proguard/tests/integration/utils.rs @@ -0,0 +1,35 @@ +use symbolicator_proguard::ProguardService; +use symbolicator_service::config::Config; +use symbolicator_service::services::SharedServices; +use symbolicator_test as test; + +pub use test::{assert_snapshot, fixture, read_fixture, source_config, symbol_server, Server}; + +/// Setup tests and create a test service. +/// +/// This function returns a tuple containing the service to test, and a temporary cache +/// directory. The directory is cleaned up when the [`TempDir`] instance is dropped. Keep it as +/// guard until the test has finished. +/// +/// The service is configured with `connect_to_reserved_ips = True`. This allows to use a local +/// symbol server to test object file downloads. +/// The `update_config` closure can modify any default configuration if needed before the server is +/// started. +pub fn setup_service(update_config: impl FnOnce(&mut Config)) -> (ProguardService, test::TempDir) { + test::setup(); + + let cache_dir = test::tempdir(); + + let mut config = Config { + cache_dir: Some(cache_dir.path().to_owned()), + connect_to_reserved_ips: true, + ..Default::default() + }; + update_config(&mut config); + + let handle = tokio::runtime::Handle::current(); + let shared_services = SharedServices::new(config, handle).unwrap(); + let proguard = ProguardService::new(&shared_services); + + (proguard, cache_dir) +} diff --git a/crates/symbolicator-service/src/caches/versions.rs b/crates/symbolicator-service/src/caches/versions.rs index 6e435c38c..e8ac2c18a 100644 --- a/crates/symbolicator-service/src/caches/versions.rs +++ b/crates/symbolicator-service/src/caches/versions.rs @@ -138,3 +138,11 @@ pub const BUNDLE_INDEX_CACHE_VERSIONS: CacheVersions = CacheVersions { current: 1, fallbacks: &[], }; + +/// Proguard Cache, with the following versions: +/// +/// - `1`: Initial version. +pub const PROGUARD_CACHE_VERSIONS: CacheVersions = CacheVersions { + current: 1, + fallbacks: &[], +}; diff --git a/crates/symbolicator-service/src/caching/cleanup.rs b/crates/symbolicator-service/src/caching/cleanup.rs index d1cc00b6a..9ca171f46 100644 --- a/crates/symbolicator-service/src/caching/cleanup.rs +++ b/crates/symbolicator-service/src/caching/cleanup.rs @@ -47,6 +47,7 @@ impl Caches { sourcefiles, bundle_index, diagnostics, + proguard, } = &self; // We want to clean up the caches in a random order. Ideally, this should not matter at all, @@ -68,6 +69,7 @@ impl Caches { sourcefiles, bundle_index, diagnostics, + proguard, ]; let mut rng = thread_rng(); caches.as_mut_slice().shuffle(&mut rng); diff --git a/crates/symbolicator-service/src/caching/config.rs b/crates/symbolicator-service/src/caching/config.rs index cf42a7e4f..45397f5f3 100644 --- a/crates/symbolicator-service/src/caching/config.rs +++ b/crates/symbolicator-service/src/caching/config.rs @@ -14,6 +14,7 @@ pub enum CacheName { SourceFiles, BundleIndex, Diagnostics, + Proguard, } impl AsRef for CacheName { @@ -30,6 +31,7 @@ impl AsRef for CacheName { Self::SourceFiles => "sourcefiles", Self::BundleIndex => "bundle_index", Self::Diagnostics => "diagnostics", + Self::Proguard => "proguard", } } } diff --git a/crates/symbolicator-service/src/caching/mod.rs b/crates/symbolicator-service/src/caching/mod.rs index 7e26e6ebe..05d21ad25 100644 --- a/crates/symbolicator-service/src/caching/mod.rs +++ b/crates/symbolicator-service/src/caching/mod.rs @@ -197,6 +197,7 @@ pub struct Caches { pub bundle_index: Cache, /// Store for minidump data symbolicator failed to process, for diagnostics purposes pub diagnostics: Cache, + pub proguard: Cache, } impl Caches { @@ -287,7 +288,7 @@ impl Caches { CacheName::BundleIndex, config, config.caches.downloaded.into(), - max_lazy_redownloads, + max_lazy_redownloads.clone(), in_memory.bundle_index_capacity, )?, diagnostics: Cache::from_config( @@ -297,6 +298,13 @@ impl Caches { Default::default(), default_cap, )?, + proguard: Cache::from_config( + CacheName::Proguard, + config, + config.caches.downloaded.into(), + max_lazy_redownloads, + default_cap, + )?, }) } } diff --git a/crates/symbolicator-service/src/download/sentry.rs b/crates/symbolicator-service/src/download/sentry.rs index d2da4b64c..011277132 100644 --- a/crates/symbolicator-service/src/download/sentry.rs +++ b/crates/symbolicator-service/src/download/sentry.rs @@ -47,6 +47,7 @@ enum SentryFileType { UuidMap, BcSymbolMap, Il2cpp, + Proguard, } impl From for SentryFileType { @@ -63,6 +64,7 @@ impl From for SentryFileType { FileType::UuidMap => Self::UuidMap, FileType::BcSymbolMap => Self::BcSymbolMap, FileType::Il2cpp => Self::Il2cpp, + FileType::Proguard => Self::Proguard, } } } diff --git a/crates/symbolicator-sources/src/filetype.rs b/crates/symbolicator-sources/src/filetype.rs index ec5fe4fdd..8db2d7d4b 100644 --- a/crates/symbolicator-sources/src/filetype.rs +++ b/crates/symbolicator-sources/src/filetype.rs @@ -55,6 +55,8 @@ pub enum FileType { /// This file maps from C++ source locations to the original C# source location it was transpiled from. #[serde(rename = "il2cpp")] Il2cpp, + /// A proguard debug file. + Proguard, } impl FileType { @@ -76,6 +78,7 @@ impl FileType { UuidMap, BcSymbolMap, PortablePdb, + Proguard, ] } @@ -138,6 +141,7 @@ impl AsRef for FileType { FileType::BcSymbolMap => "bcsymbolmap", FileType::Il2cpp => "il2cpp", FileType::PortablePdb => "portablepdb", + FileType::Proguard => "proguard", } } } diff --git a/crates/symbolicator-sources/src/paths.rs b/crates/symbolicator-sources/src/paths.rs index 02106b133..aabaf1de9 100644 --- a/crates/symbolicator-sources/src/paths.rs +++ b/crates/symbolicator-sources/src/paths.rs @@ -206,6 +206,7 @@ fn get_native_paths(filetype: FileType, identifier: &ObjectId) -> Vec { FileType::UuidMap => Vec::new(), FileType::BcSymbolMap => Vec::new(), FileType::Il2cpp => Vec::new(), + FileType::Proguard => Vec::new(), } } @@ -283,6 +284,7 @@ fn get_symstore_path( FileType::UuidMap => None, FileType::BcSymbolMap => None, FileType::Il2cpp => None, + FileType::Proguard => None, } } @@ -329,6 +331,7 @@ fn get_debuginfod_path(filetype: FileType, identifier: &ObjectId) -> Option None, FileType::BcSymbolMap => None, FileType::Il2cpp => None, + FileType::Proguard => None, } } @@ -384,6 +387,7 @@ fn get_search_target_id(filetype: FileType, identifier: &ObjectId) -> Option { Some(Cow::Borrowed(identifier.code_id.as_ref()?.as_str())) } + FileType::Proguard => None, } } @@ -401,6 +405,7 @@ fn get_unified_path(filetype: FileType, identifier: &ObjectId) -> Option FileType::UuidMap => "uuidmap", FileType::BcSymbolMap => "bcsymbolmap", FileType::Il2cpp => "il2cpp", + FileType::Proguard => "proguard", }; // determine the ID we use for the path diff --git a/crates/symbolicator-stress/Cargo.toml b/crates/symbolicator-stress/Cargo.toml index 578f947c0..63586fb82 100644 --- a/crates/symbolicator-stress/Cargo.toml +++ b/crates/symbolicator-stress/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0.81" serde_yaml = "0.9.14" symbolicator-js = { path = "../symbolicator-js" } symbolicator-native = { path = "../symbolicator-native" } +symbolicator-proguard = { path = "../symbolicator-proguard" } symbolicator-service = { path = "../symbolicator-service" } symbolicator-test = { path = "../symbolicator-test" } tempfile = "3.2.0" diff --git a/crates/symbolicator-test/src/lib.rs b/crates/symbolicator-test/src/lib.rs index d84301689..b98c83475 100644 --- a/crates/symbolicator-test/src/lib.rs +++ b/crates/symbolicator-test/src/lib.rs @@ -16,7 +16,7 @@ //! source) = symbol_server();`. Alternatively, use [`local_source`] to test without //! HTTP connections. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::net::{SocketAddr, TcpListener}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -392,6 +392,47 @@ where (server, source) } +pub fn sentry_server(fixtures_dir: impl AsRef, lookup: L) -> (Server, SentrySourceConfig) +where + L: Fn(&str, &HashMap) -> serde_json::Value + Clone + Send + 'static, +{ + let files_url = Arc::new(OnceCell::::new()); + + let router = { + let files_url = files_url.clone(); + + let tracing_layer = TraceLayer::new_for_http().on_response(()); + let serve_dir = get_service(ServeDir::new(fixtures_dir)); + Router::new() + .route( + "/files/dsyms/", + get( + move |extract::Query(params): extract::Query>| async move { + let files_url = files_url.get().unwrap().as_str(); + if let Some(download_id) = params.get("id") { + return Redirect::to(&format!("/files/{download_id}")).into_response(); + } + let res = lookup(files_url, ¶ms); + Json(res).into_response() + }, + ), + ) + .nest_service("/files", serve_dir) + .layer(tracing_layer) + }; + let server = Server::with_router(router); + + files_url.set(server.url("/files")).unwrap(); + + let source = SentrySourceConfig { + id: SourceId::new("sentry:project"), + url: server.url("/files/dsyms/"), + token: String::new(), + }; + + (server, source) +} + /// Returns the legacy read-only GCS credentials for testing GCS support. /// /// Use the `gcs_source_key!()` macro instead which will skip correctly. diff --git a/crates/symbolicator/Cargo.toml b/crates/symbolicator/Cargo.toml index e250884e0..abf975bbf 100644 --- a/crates/symbolicator/Cargo.toml +++ b/crates/symbolicator/Cargo.toml @@ -24,6 +24,7 @@ symbolic = "12.7.1" symbolicator-crash = { path = "../symbolicator-crash", optional = true } symbolicator-js = { path = "../symbolicator-js" } symbolicator-native = { path = "../symbolicator-native" } +symbolicator-proguard = { path = "../symbolicator-proguard" } symbolicator-service = { path = "../symbolicator-service" } symbolicator-sources = { path = "../symbolicator-sources" } tempfile = "3.2.0" diff --git a/crates/symbolicator/src/service.rs b/crates/symbolicator/src/service.rs index a0852dc0a..6355bb7be 100644 --- a/crates/symbolicator/src/service.rs +++ b/crates/symbolicator/src/service.rs @@ -28,6 +28,8 @@ use symbolicator_js::interface::{CompletedJsSymbolicationResponse, SymbolicateJs use symbolicator_js::SourceMapService; use symbolicator_native::interface::{CompletedSymbolicationResponse, SymbolicateStacktraces}; use symbolicator_native::SymbolicationActor; +// TODO: actually use it +use symbolicator_proguard as _; use symbolicator_service::caching::CacheEntry; use symbolicator_service::config::Config; use symbolicator_service::metric; diff --git a/tests/fixtures/proguard/01/proguard.txt b/tests/fixtures/proguard/01/proguard.txt new file mode 100644 index 000000000..4168d1c0e --- /dev/null +++ b/tests/fixtures/proguard/01/proguard.txt @@ -0,0 +1,35 @@ +android.support.constraint.ConstraintLayout$LayoutParams -> android.support.constraint.ConstraintLayout$a: + int guideBegin -> a +android.support.constraint.solver.ArrayRow -> android.support.constraint.a.b: + android.support.constraint.solver.SolverVariable variable -> a + float constantValue -> b + boolean used -> c + android.support.constraint.solver.ArrayLinkedVariables variables -> d + boolean isSimpleDefinition -> e + 22:32:void (android.support.constraint.solver.Cache) -> + 35:36:void updateClientEquations() -> a + 43:43:boolean hasKeyVariable() -> b + 51:51:java.lang.String toString() -> toString + 55:101:java.lang.String toReadableString() -> c + 105:109:void reset() -> d + 112:112:boolean hasVariable(android.support.constraint.solver.SolverVariable) -> a + 116:120:android.support.constraint.solver.ArrayRow createRowDefinition(android.support.constraint.solver.SolverVariable,int) -> a + 124:131:android.support.constraint.solver.ArrayRow createRowEquals(android.support.constraint.solver.SolverVariable,int) -> b + 135:151:android.support.constraint.solver.ArrayRow createRowEquals(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,int) -> a + 155:156:android.support.constraint.solver.ArrayRow addSingleError(android.support.constraint.solver.SolverVariable,int) -> c + 162:180:android.support.constraint.solver.ArrayRow createRowGreaterThan(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,int) -> a + 185:203:android.support.constraint.solver.ArrayRow createRowLowerThan(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,int) -> b + 211:233:android.support.constraint.solver.ArrayRow createRowEqualDimension(float,float,float,android.support.constraint.solver.SolverVariable,int,android.support.constraint.solver.SolverVariable,int,android.support.constraint.solver.SolverVariable,int,android.support.constraint.solver.SolverVariable,int) -> a + 238:280:android.support.constraint.solver.ArrayRow createRowCentering(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,int,float,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,int) -> a + 284:286:android.support.constraint.solver.ArrayRow addError(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable) -> a + 291:294:android.support.constraint.solver.ArrayRow createRowDimensionPercent(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,float) -> a + 311:315:android.support.constraint.solver.ArrayRow createRowDimensionRatio(android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,android.support.constraint.solver.SolverVariable,float) -> a + 331:332:boolean updateRowWithEquation(android.support.constraint.solver.ArrayRow) -> a + 337:342:void ensurePositiveConstant() -> e + 345:352:void pickRowVariable() -> f + 355:368:void pivot(android.support.constraint.solver.SolverVariable) -> b +io.sentry.sample.MainActivity -> io.sentry.sample.MainActivity: + 1:1:void ():15:15 -> + 1:1:void bar():54:54 -> a + 1:1:void foo():44 -> a + 1:1:void onClickHandler(android.view.View):40 -> a