Merge topic 'rest-caches'
* changes: Add REST endpoint to get a cache Add REST endpoint to list caches Use '-' in cache name as separator between plugin name and cache name
This commit is contained in:
commit
5ced398580
@ -30,6 +30,261 @@ Returns the version of the Gerrit server.
|
||||
"2.7"
|
||||
----
|
||||
|
||||
[[list-caches]]
|
||||
=== List Caches
|
||||
--
|
||||
'GET /config/server/caches/'
|
||||
--
|
||||
|
||||
Lists the caches of the server. Caches defined by plugins are included.
|
||||
|
||||
The caller must be a member of a group that is granted the
|
||||
link:access-control.html#capability_viewCaches[View Caches] capability
|
||||
or the link:access-control.html#capability_administrateServer[
|
||||
Administrate Server] capability.
|
||||
|
||||
As result a map of link:#cache-info[CacheInfo] entities is returned.
|
||||
|
||||
The entries in the map are sorted by cache name.
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /config/server/caches/ HTTP/1.0
|
||||
----
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
)]}'
|
||||
{
|
||||
"accounts": {
|
||||
"entries": {
|
||||
"mem": 4
|
||||
},
|
||||
"average_get": "2.5ms",
|
||||
"hit_ratio": {
|
||||
"mem": 94
|
||||
}
|
||||
},
|
||||
"accounts_byemail": {
|
||||
"entries": {
|
||||
"mem": 4
|
||||
},
|
||||
"average_get": "771.8us",
|
||||
"hit_ratio": {
|
||||
"mem": 95
|
||||
}
|
||||
},
|
||||
"accounts_byname": {
|
||||
"entries": {
|
||||
"mem": 4
|
||||
},
|
||||
"hit_ratio": {
|
||||
"mem": 100
|
||||
}
|
||||
},
|
||||
"adv_bases": {
|
||||
"entries": {},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"change_kind": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"space": "0.00k"
|
||||
},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"changes": {
|
||||
"entries": {},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"conflicts": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"mem": 2,
|
||||
"disk": 3,
|
||||
"space": "2.75k"
|
||||
},
|
||||
"hit_ratio": {
|
||||
"mem": 0,
|
||||
"disk": 100
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"mem": 177,
|
||||
"disk": 253,
|
||||
"space": "170.97k"
|
||||
},
|
||||
"average_get": "1.1ms",
|
||||
"hit_ratio": {
|
||||
"mem": 67,
|
||||
"disk": 100
|
||||
}
|
||||
},
|
||||
"diff_intraline": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"mem": 1,
|
||||
"disk": 1,
|
||||
"space": "0.37k"
|
||||
},
|
||||
"average_get": "6.8ms",
|
||||
"hit_ratio": {
|
||||
"mem": 0
|
||||
}
|
||||
},
|
||||
"git_tags": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"space": "0.00k"
|
||||
},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
groups": {
|
||||
"entries": {
|
||||
"mem": 27
|
||||
},
|
||||
"average_get": "183.2us",
|
||||
"hit_ratio": {
|
||||
"mem": 12
|
||||
}
|
||||
},
|
||||
"groups_byinclude": {
|
||||
"entries": {},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"groups_byname": {
|
||||
"entries": {},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"groups_byuuid": {
|
||||
"entries": {
|
||||
"mem": 25
|
||||
},
|
||||
"average_get": "173.4us",
|
||||
"hit_ratio": {
|
||||
"mem": 13
|
||||
}
|
||||
},
|
||||
"groups_external": {
|
||||
"entries": {},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
groups_members": {
|
||||
"entries": {
|
||||
"mem": 4
|
||||
},
|
||||
"average_get": "697.8us",
|
||||
"hit_ratio": {
|
||||
"mem": 82
|
||||
}
|
||||
},
|
||||
"permission_sort": {
|
||||
"entries": {
|
||||
"mem": 16
|
||||
},
|
||||
"hit_ratio": {
|
||||
"mem": 96
|
||||
}
|
||||
},
|
||||
"plugin_resources": {
|
||||
"entries": {
|
||||
"mem": 2
|
||||
},
|
||||
"hit_ratio": {
|
||||
"mem": 83
|
||||
}
|
||||
},
|
||||
"project_list": {
|
||||
"entries": {
|
||||
"mem": 1
|
||||
},
|
||||
"average_get": "18.6ms",
|
||||
"hit_ratio": {
|
||||
"mem": 0
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"entries": {
|
||||
"mem": 35
|
||||
},
|
||||
"average_get": "8.6ms",
|
||||
"hit_ratio": {
|
||||
"mem": 99
|
||||
}
|
||||
},
|
||||
"quota-repo_size": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"space": "0.00k"
|
||||
},
|
||||
"hit_ratio": {}
|
||||
},
|
||||
"sshkeys": {
|
||||
"entries": {
|
||||
"mem": 1
|
||||
},
|
||||
"average_get": "3.2ms",
|
||||
"hit_ratio": {
|
||||
"mem": 50
|
||||
}
|
||||
},
|
||||
"web_sessions": {
|
||||
"type": "DISK",
|
||||
"entries": {
|
||||
"mem": 1,
|
||||
"disk": 2,
|
||||
"space": "0.78k"
|
||||
},
|
||||
"hit_ratio": {
|
||||
"mem": 82
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[get-cache]]
|
||||
=== Get Cache
|
||||
--
|
||||
'GET /config/server/caches/link:#cache-name[\{cache-name\}]'
|
||||
--
|
||||
|
||||
Retrieves information about a cache.
|
||||
|
||||
The caller must be a member of a group that is granted the
|
||||
link:access-control.html#capability_viewCaches[View Caches] capability
|
||||
or the link:access-control.html#capability_administrateServer[
|
||||
Administrate Server] capability.
|
||||
|
||||
As result a link:#cache-info[CacheInfo] entity is returned.
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /config/server/caches/projects HTTP/1.0
|
||||
----
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
)]}'
|
||||
{
|
||||
"name": "projects",
|
||||
"entries": {
|
||||
"mem": 35
|
||||
},
|
||||
"average_get": " 8.6ms",
|
||||
"hit_ratio": {
|
||||
"mem": 99
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[list-capabilities]]
|
||||
=== List Capabilities
|
||||
--
|
||||
@ -176,9 +431,48 @@ link:#top-menu-entry-info[TopMenuEntryInfo] entities is returned.
|
||||
----
|
||||
|
||||
|
||||
[[ids]]
|
||||
== IDs
|
||||
|
||||
[[cache-name]]
|
||||
=== \{cache-name\}
|
||||
The name of the cache.
|
||||
|
||||
If the cache is defined by a plugin the cache name must include the
|
||||
plugin name: "<plugin-name>-<cache-name>".
|
||||
|
||||
Gerrit core caches can optionally be prefixed with "gerrit":
|
||||
"gerrit-<cache-name>".
|
||||
|
||||
|
||||
[[json-entities]]
|
||||
== JSON Entities
|
||||
|
||||
[[cache-info]]
|
||||
=== CacheInfo
|
||||
The `CacheInfo` entity contains information about a cache.
|
||||
|
||||
[options="header",width="50%",cols="1,^1,5"]
|
||||
|==================================
|
||||
|Field Name ||Description
|
||||
|`name` |
|
||||
not set if returned in a map where the cache name is used as map key|
|
||||
The cache name. If the cache is defined by a plugin the cache name
|
||||
includes the plugin name: "<plugin-name>-<cache-name>".
|
||||
|`type` |not set for in memory caches|
|
||||
The type of the cache (`MEM`: in memory cache, `DISK`: disk cache).
|
||||
|`entries` ||
|
||||
Information about the entries in the cache as a
|
||||
link:#entries-info[EntriesInfo] entity.
|
||||
|`average_get` |optional|
|
||||
The average duration of getting one entry from the cache. The value is
|
||||
returned with a standard time unit abbreviation (`ns`: nanoseconds,
|
||||
`us`: microseconds, `ms`: milliseconds, `s`: seconds).
|
||||
|`hit_ratio` ||
|
||||
Information about the hit ratio as a link:#hit-ration-info[
|
||||
HitRatioInfo] entity.
|
||||
|==================================
|
||||
|
||||
[[capability-info]]
|
||||
=== CapabilityInfo
|
||||
The `CapabilityInfo` entity contains information about a capability.
|
||||
@ -191,6 +485,39 @@ The `CapabilityInfo` entity contains information about a capability.
|
||||
|`name` |capability name
|
||||
|=================================
|
||||
|
||||
[[entries-info]]
|
||||
=== EntriesInfo
|
||||
The `EntriesInfo` entity contains information about the entries in a
|
||||
cache.
|
||||
|
||||
[options="header",width="50%",cols="1,^1,5"]
|
||||
|==================================
|
||||
|Field Name ||Description
|
||||
|`mem` |optional|Number of cache entries that are held in memory.
|
||||
|`disk` |optional|Number of cache entries on the disk. For non-disk
|
||||
caches this value is not set; for disk caches it is only set if there
|
||||
are entries in the cache.
|
||||
|`space` |optional|
|
||||
The space that is consumed by the cache on disk. The value is returned
|
||||
with a unit abbreviation (`k`: kilobytes, `m`: megabytes,
|
||||
`g`: gigabytes). Only set for disk caches.
|
||||
|==================================
|
||||
|
||||
[[hit-ration-info]]
|
||||
=== HitRatioInfo
|
||||
The `HitRatioInfo` entity contains information about the hit ratio of a
|
||||
cache.
|
||||
|
||||
[options="header",width="50%",cols="1,^1,5"]
|
||||
|==================================
|
||||
|Field Name ||Description
|
||||
|`mem` ||
|
||||
Hit ratio for cache entries that are held in memory (0 \<= value \<= 100).
|
||||
|`disk` |optional|
|
||||
Hit ratio for cache entries that are held on disk (0 \<= value \<= 100).
|
||||
Only set for disk caches.
|
||||
|==================================
|
||||
|
||||
[[top-menu-entry-info]]
|
||||
=== TopMenuEntryInfo
|
||||
The `TopMenuEntryInfo` entity contains information about a top menu
|
||||
|
@ -0,0 +1,6 @@
|
||||
include_defs('//gerrit-acceptance-tests/tests.defs')
|
||||
|
||||
acceptance_tests(
|
||||
srcs = glob(['*IT.java']),
|
||||
labels = ['rest']
|
||||
)
|
@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.acceptance.rest.config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||
import com.google.gerrit.acceptance.RestResponse;
|
||||
import com.google.gerrit.server.config.ListCaches.CacheInfo;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GetCacheIT extends AbstractDaemonTest {
|
||||
|
||||
@Test
|
||||
public void getCache() throws IOException {
|
||||
RestResponse r = adminSession.get("/config/server/caches/accounts");
|
||||
assertEquals(HttpStatus.SC_OK, r.getStatusCode());
|
||||
CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
|
||||
|
||||
assertEquals("accounts", result.name);
|
||||
assertNull(result.type);
|
||||
assertEquals(1, result.entries.mem.longValue());
|
||||
assertNotNull(result.averageGet);
|
||||
assertTrue(result.averageGet.endsWith("s"));
|
||||
assertNull(result.entries.disk);
|
||||
assertNull(result.entries.space);
|
||||
assertTrue(result.hitRatio.mem >= 0);
|
||||
assertTrue(result.hitRatio.mem <= 100);
|
||||
assertNull(result.hitRatio.disk);
|
||||
|
||||
userSession.get("/config/server/version").consume();
|
||||
r = adminSession.get("/config/server/caches/accounts");
|
||||
assertEquals(HttpStatus.SC_OK, r.getStatusCode());
|
||||
result = newGson().fromJson(r.getReader(), CacheInfo.class);
|
||||
assertEquals(2, result.entries.mem.longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCache_Forbidden() throws IOException {
|
||||
RestResponse r = userSession.get("/config/server/caches/accounts");
|
||||
assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCache_NotFound() throws IOException {
|
||||
RestResponse r = adminSession.get("/config/server/caches/nonExisting");
|
||||
assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCacheWithGerritPrefix() throws IOException {
|
||||
RestResponse r = adminSession.get("/config/server/caches/gerrit-accounts");
|
||||
assertEquals(HttpStatus.SC_OK, r.getStatusCode());
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.acceptance.rest.config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||
import com.google.gerrit.acceptance.RestResponse;
|
||||
import com.google.gerrit.server.config.ListCaches.CacheInfo;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public class ListCachesIT extends AbstractDaemonTest {
|
||||
|
||||
@Test
|
||||
public void listCaches() throws IOException {
|
||||
RestResponse r = adminSession.get("/config/server/caches/");
|
||||
assertEquals(HttpStatus.SC_OK, r.getStatusCode());
|
||||
Map<String, CacheInfo> result =
|
||||
newGson().fromJson(r.getReader(),
|
||||
new TypeToken<Map<String, CacheInfo>>() {}.getType());
|
||||
|
||||
assertTrue(result.containsKey("accounts"));
|
||||
CacheInfo accountsCacheInfo = result.get("accounts");
|
||||
assertNull(accountsCacheInfo.type);
|
||||
assertEquals(1, accountsCacheInfo.entries.mem.longValue());
|
||||
assertNotNull(accountsCacheInfo.averageGet);
|
||||
assertTrue(accountsCacheInfo.averageGet.endsWith("s"));
|
||||
assertNull(accountsCacheInfo.entries.disk);
|
||||
assertNull(accountsCacheInfo.entries.space);
|
||||
assertTrue(accountsCacheInfo.hitRatio.mem >= 0);
|
||||
assertTrue(accountsCacheInfo.hitRatio.mem <= 100);
|
||||
assertNull(accountsCacheInfo.hitRatio.disk);
|
||||
|
||||
userSession.get("/config/server/version").consume();
|
||||
r = adminSession.get("/config/server/caches/");
|
||||
assertEquals(HttpStatus.SC_OK, r.getStatusCode());
|
||||
result = newGson().fromJson(r.getReader(),
|
||||
new TypeToken<Map<String, CacheInfo>>() {}.getType());
|
||||
assertEquals(2, result.get("accounts").entries.mem.longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listCaches_Forbidden() throws IOException {
|
||||
RestResponse r = userSession.get("/config/server/caches/");
|
||||
assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import com.google.common.hash.BloomFilter;
|
||||
import com.google.common.hash.Funnel;
|
||||
import com.google.common.hash.Funnels;
|
||||
import com.google.common.hash.PrimitiveSink;
|
||||
import com.google.gerrit.server.cache.PersistentCache;
|
||||
import com.google.gerrit.server.util.TimeUtil;
|
||||
import com.google.inject.TypeLiteral;
|
||||
|
||||
@ -63,7 +64,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
*
|
||||
* @see H2CacheFactory
|
||||
*/
|
||||
public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements
|
||||
PersistentCache {
|
||||
private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
|
||||
|
||||
private final Executor executor;
|
||||
@ -156,6 +158,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
return mem.stats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiskStats diskStats() {
|
||||
return store.diskStats();
|
||||
}
|
||||
@ -193,29 +196,6 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public static class DiskStats {
|
||||
long size;
|
||||
long space;
|
||||
long hitCount;
|
||||
long missCount;
|
||||
|
||||
public long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public long space() {
|
||||
return space;
|
||||
}
|
||||
|
||||
public long hitCount() {
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
public long requestCount() {
|
||||
return hitCount + missCount;
|
||||
}
|
||||
}
|
||||
|
||||
static class ValueHolder<V> {
|
||||
final V value;
|
||||
long created;
|
||||
@ -599,9 +579,8 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
}
|
||||
|
||||
DiskStats diskStats() {
|
||||
DiskStats d = new DiskStats();
|
||||
d.hitCount = hitCount.get();
|
||||
d.missCount = missCount.get();
|
||||
long size = 0;
|
||||
long space = 0;
|
||||
SqlHandle c = null;
|
||||
try {
|
||||
c = acquire();
|
||||
@ -613,8 +592,8 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
+ " FROM data");
|
||||
try {
|
||||
if (r.next()) {
|
||||
d.size = r.getLong(1);
|
||||
d.space = r.getLong(2);
|
||||
size = r.getLong(1);
|
||||
space = r.getLong(2);
|
||||
}
|
||||
} finally {
|
||||
r.close();
|
||||
@ -628,7 +607,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
|
||||
} finally {
|
||||
release(c);
|
||||
}
|
||||
return d;
|
||||
return new DiskStats(size, space, hitCount.get(), missCount.get());
|
||||
}
|
||||
|
||||
private SqlHandle acquire() throws SQLException {
|
||||
|
50
gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCache.java
vendored
Normal file
50
gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCache.java
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.cache;
|
||||
|
||||
public interface PersistentCache {
|
||||
|
||||
DiskStats diskStats();
|
||||
|
||||
public static class DiskStats {
|
||||
private final long size;
|
||||
private final long space;
|
||||
private final long hitCount;
|
||||
private final long missCount;
|
||||
|
||||
public DiskStats(long size, long space, long hitCount, long missCount) {
|
||||
this.size = size;
|
||||
this.space = space;
|
||||
this.hitCount = hitCount;
|
||||
this.missCount = missCount;
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public long space() {
|
||||
return space;
|
||||
}
|
||||
|
||||
public long hitCount() {
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
public long requestCount() {
|
||||
return hitCount + missCount;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.config;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.TypeLiteral;
|
||||
|
||||
public class CacheResource extends ConfigResource {
|
||||
public static final TypeLiteral<RestView<CacheResource>> CACHE_KIND =
|
||||
new TypeLiteral<RestView<CacheResource>>() {};
|
||||
|
||||
private final String name;
|
||||
private final Provider<Cache<?, ?>> cacheProvider;
|
||||
|
||||
public CacheResource(String pluginName, String cacheName, Provider<Cache<?, ?>> cacheProvider) {
|
||||
this.name = cacheNameOf(pluginName, cacheName);
|
||||
this.cacheProvider = cacheProvider;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Cache<?, ?> getCache() {
|
||||
return cacheProvider.get();
|
||||
}
|
||||
|
||||
public static String cacheNameOf(String plugin, String name) {
|
||||
if ("gerrit".equals(plugin)) {
|
||||
return name;
|
||||
} else {
|
||||
return plugin + "-" + name;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.config;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.gerrit.common.data.GlobalCapability;
|
||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.ChildCollection;
|
||||
import com.google.gerrit.extensions.restapi.IdString;
|
||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.gerrit.server.AnonymousUser;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@RequiresCapability(GlobalCapability.VIEW_CACHES)
|
||||
@Singleton
|
||||
public class CachesCollection implements
|
||||
ChildCollection<ConfigResource, CacheResource> {
|
||||
|
||||
private final DynamicMap<RestView<CacheResource>> views;
|
||||
private final Provider<ListCaches> list;
|
||||
private final Provider<CurrentUser> self;
|
||||
private final DynamicMap<Cache<?, ?>> cacheMap;
|
||||
|
||||
@Inject
|
||||
CachesCollection(DynamicMap<RestView<CacheResource>> views,
|
||||
Provider<ListCaches> list, Provider<CurrentUser> self,
|
||||
DynamicMap<Cache<?, ?>> cacheMap) {
|
||||
this.views = views;
|
||||
this.list = list;
|
||||
this.self = self;
|
||||
this.cacheMap = cacheMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestView<ConfigResource> list() {
|
||||
return list.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheResource parse(ConfigResource parent, IdString id)
|
||||
throws AuthException, ResourceNotFoundException {
|
||||
CurrentUser user = self.get();
|
||||
if (user instanceof AnonymousUser) {
|
||||
throw new AuthException("Authentication required");
|
||||
} else if (!user.isIdentifiedUser()) {
|
||||
throw new ResourceNotFoundException();
|
||||
} else if (!user.getCapabilities().canViewCaches()) {
|
||||
throw new AuthException("not allowed to view caches");
|
||||
}
|
||||
|
||||
String cacheName = id.get();
|
||||
String pluginName = "gerrit";
|
||||
int i = cacheName.lastIndexOf('-');
|
||||
if (i != -1) {
|
||||
pluginName = cacheName.substring(0, i);
|
||||
cacheName = cacheName.length() > i + 1 ? cacheName.substring(i + 1) : "";
|
||||
}
|
||||
|
||||
Provider<Cache<?, ?>> cacheProvider = cacheMap.byPlugin(pluginName).get(cacheName);
|
||||
if (cacheProvider == null) {
|
||||
throw new ResourceNotFoundException(id);
|
||||
}
|
||||
return new CacheResource(pluginName, cacheName, cacheProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DynamicMap<RestView<CacheResource>> views() {
|
||||
return views;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.config;
|
||||
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.server.config.ListCaches.CacheInfo;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class GetCache implements RestReadView<CacheResource> {
|
||||
|
||||
@Override
|
||||
public CacheInfo apply(CacheResource rsrc) {
|
||||
return new CacheInfo(rsrc.getName(), rsrc.getCache());
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.config;
|
||||
|
||||
import static com.google.gerrit.server.config.CacheResource.cacheNameOf;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheStats;
|
||||
import com.google.gerrit.common.data.GlobalCapability;
|
||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.server.cache.PersistentCache;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@RequiresCapability(GlobalCapability.VIEW_CACHES)
|
||||
@Singleton
|
||||
public class ListCaches implements RestReadView<ConfigResource> {
|
||||
private final DynamicMap<Cache<?, ?>> cacheMap;
|
||||
|
||||
@Inject
|
||||
public ListCaches(DynamicMap<Cache<?, ?>> cacheMap) {
|
||||
this.cacheMap = cacheMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, CacheInfo> apply(ConfigResource rsrc) {
|
||||
Map<String, CacheInfo> cacheInfos = new TreeMap<>();
|
||||
for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
|
||||
cacheInfos.put(cacheNameOf(e.getPluginName(), e.getExportName()),
|
||||
new CacheInfo(e.getProvider().get()));
|
||||
}
|
||||
return cacheInfos;
|
||||
}
|
||||
|
||||
public enum CacheType {
|
||||
MEM, DISK;
|
||||
}
|
||||
|
||||
public static class CacheInfo {
|
||||
public String name;
|
||||
public CacheType type;
|
||||
public EntriesInfo entries;
|
||||
public String averageGet;
|
||||
public HitRatioInfo hitRatio;
|
||||
|
||||
public CacheInfo(Cache<?,?> cache) {
|
||||
this(null, cache);
|
||||
}
|
||||
|
||||
public CacheInfo(String name, Cache<?,?> cache) {
|
||||
this.name = name;
|
||||
|
||||
CacheStats stat = cache.stats();
|
||||
|
||||
entries = new EntriesInfo();
|
||||
entries.setMem(cache.size());
|
||||
|
||||
averageGet = duration(stat.averageLoadPenalty());
|
||||
|
||||
hitRatio = new HitRatioInfo();
|
||||
hitRatio.setMem(stat.hitCount(), stat.requestCount());
|
||||
|
||||
if (cache instanceof PersistentCache) {
|
||||
type = CacheType.DISK;
|
||||
PersistentCache.DiskStats diskStats =
|
||||
((PersistentCache) cache).diskStats();
|
||||
entries.setDisk(diskStats.size());
|
||||
entries.setSpace(diskStats.space());
|
||||
hitRatio.setDisk(diskStats.hitCount(), diskStats.requestCount());
|
||||
}
|
||||
}
|
||||
|
||||
private static String duration(double ns) {
|
||||
if (ns < 0.5) {
|
||||
return null;
|
||||
}
|
||||
String suffix = "ns";
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "us";
|
||||
}
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "ms";
|
||||
}
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "s";
|
||||
}
|
||||
return String.format("%4.1f%s", ns, suffix).trim();
|
||||
}
|
||||
}
|
||||
|
||||
public static class EntriesInfo {
|
||||
public Long mem;
|
||||
public Long disk;
|
||||
public String space;
|
||||
|
||||
public void setMem(long mem) {
|
||||
this.mem = mem != 0 ? mem : null;
|
||||
}
|
||||
|
||||
public void setDisk(long disk) {
|
||||
this.disk = disk != 0 ? disk : null;
|
||||
}
|
||||
|
||||
public void setSpace(double value) {
|
||||
space = bytes(value);
|
||||
}
|
||||
|
||||
private static String bytes(double value) {
|
||||
value /= 1024;
|
||||
String suffix = "k";
|
||||
|
||||
if (value > 1024) {
|
||||
value /= 1024;
|
||||
suffix = "m";
|
||||
}
|
||||
if (value > 1024) {
|
||||
value /= 1024;
|
||||
suffix = "g";
|
||||
}
|
||||
return String.format("%1$6.2f%2$s", value, suffix).trim();
|
||||
}
|
||||
}
|
||||
|
||||
public static class HitRatioInfo {
|
||||
public Integer mem;
|
||||
public Integer disk;
|
||||
|
||||
public void setMem(long value, long total) {
|
||||
mem = percent(value, total);
|
||||
}
|
||||
|
||||
public void setDisk(long value, long total) {
|
||||
disk = percent(value, total);
|
||||
}
|
||||
|
||||
private static Integer percent(long value, long total) {
|
||||
if (total <= 0) {
|
||||
return null;
|
||||
}
|
||||
return (int) ((100 * value) / total);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.server.config;
|
||||
|
||||
import static com.google.gerrit.server.config.CacheResource.CACHE_KIND;
|
||||
import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
|
||||
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
|
||||
import static com.google.gerrit.server.config.TopMenuResource.TOP_MENU_KIND;
|
||||
@ -24,9 +25,12 @@ import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||
public class Module extends RestApiModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
DynamicMap.mapOf(binder(), CACHE_KIND);
|
||||
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
|
||||
DynamicMap.mapOf(binder(), CONFIG_KIND);
|
||||
DynamicMap.mapOf(binder(), TOP_MENU_KIND);
|
||||
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
|
||||
child(CONFIG_KIND, "caches").to(CachesCollection.class);
|
||||
get(CACHE_KIND).to(GetCache.class);
|
||||
child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
|
||||
child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
|
||||
get(CONFIG_KIND, "version").to(GetVersion.class);
|
||||
|
@ -38,7 +38,7 @@ abstract class CacheCommand extends SshCommand {
|
||||
if ("gerrit".equals(plugin)) {
|
||||
return name;
|
||||
} else {
|
||||
return plugin + "." + name;
|
||||
return plugin + "-" + name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,20 +16,21 @@ package com.google.gerrit.sshd.commands;
|
||||
|
||||
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheStats;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gerrit.common.Version;
|
||||
import com.google.gerrit.common.data.GlobalCapability;
|
||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||
import com.google.gerrit.extensions.events.LifecycleListener;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.server.cache.h2.H2CacheImpl;
|
||||
import com.google.gerrit.server.config.ConfigResource;
|
||||
import com.google.gerrit.server.config.ListCaches;
|
||||
import com.google.gerrit.server.config.ListCaches.CacheInfo;
|
||||
import com.google.gerrit.server.config.ListCaches.CacheType;
|
||||
import com.google.gerrit.server.config.SitePath;
|
||||
import com.google.gerrit.server.git.WorkQueue;
|
||||
import com.google.gerrit.server.git.WorkQueue.Task;
|
||||
import com.google.gerrit.server.util.TimeUtil;
|
||||
import com.google.gerrit.sshd.CommandMetaData;
|
||||
import com.google.gerrit.sshd.SshCommand;
|
||||
import com.google.gerrit.sshd.SshDaemon;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@ -52,13 +53,12 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
|
||||
/** Show the current cache states. */
|
||||
@RequiresCapability(GlobalCapability.VIEW_CACHES)
|
||||
@CommandMetaData(name = "show-caches", description = "Display current cache statistics",
|
||||
runsAt = MASTER_OR_SLAVE)
|
||||
final class ShowCaches extends CacheCommand {
|
||||
final class ShowCaches extends SshCommand {
|
||||
private static volatile long serverStarted;
|
||||
|
||||
static class StartupListener implements LifecycleListener {
|
||||
@ -88,6 +88,9 @@ final class ShowCaches extends CacheCommand {
|
||||
@SitePath
|
||||
private File sitePath;
|
||||
|
||||
@Inject
|
||||
private Provider<ListCaches> listCaches;
|
||||
|
||||
@Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
|
||||
private int columns = 80;
|
||||
private int nw;
|
||||
@ -145,23 +148,10 @@ final class ShowCaches extends CacheCommand {
|
||||
}
|
||||
stdout.print("+---------------------+---------+---------+\n");
|
||||
|
||||
Map<String, H2CacheImpl<?, ?>> disks = Maps.newTreeMap();
|
||||
printMemoryCaches(disks, sortedCoreCaches());
|
||||
printMemoryCaches(disks, sortedPluginCaches());
|
||||
for (Map.Entry<String, H2CacheImpl<?, ?>> entry : disks.entrySet()) {
|
||||
H2CacheImpl<?, ?> cache = entry.getValue();
|
||||
CacheStats stat = cache.stats();
|
||||
H2CacheImpl.DiskStats disk = cache.diskStats();
|
||||
stdout.print(String.format(
|
||||
"D %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
|
||||
entry.getKey(),
|
||||
count(cache.size()),
|
||||
count(disk.size()),
|
||||
bytes(disk.space()),
|
||||
duration(stat.averageLoadPenalty()),
|
||||
percent(stat.hitCount(), stat.requestCount()),
|
||||
percent(disk.hitCount(), disk.requestCount())));
|
||||
}
|
||||
Collection<CacheInfo> caches = getCaches();
|
||||
printMemoryCoreCaches(caches);
|
||||
printMemoryPluginCaches(caches);
|
||||
printDiskCaches(caches);
|
||||
stdout.print('\n');
|
||||
|
||||
if (gc) {
|
||||
@ -181,46 +171,62 @@ final class ShowCaches extends CacheCommand {
|
||||
stdout.flush();
|
||||
}
|
||||
|
||||
private void printMemoryCaches(
|
||||
Map<String, H2CacheImpl<?, ?>> disks,
|
||||
Map<String, Cache<?,?>> caches) {
|
||||
for (Map.Entry<String, Cache<?,?>> entry : caches.entrySet()) {
|
||||
Cache<?,?> cache = entry.getValue();
|
||||
if (cache instanceof H2CacheImpl) {
|
||||
disks.put(entry.getKey(), (H2CacheImpl<?,?>)cache);
|
||||
continue;
|
||||
private Collection<CacheInfo> getCaches() {
|
||||
Map<String, CacheInfo> caches = listCaches.get().apply(new ConfigResource());
|
||||
for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
|
||||
CacheInfo cache = entry.getValue();
|
||||
if (cache.type == null) {
|
||||
cache.type = CacheType.MEM;
|
||||
}
|
||||
cache.name = entry.getKey();
|
||||
}
|
||||
return caches.values();
|
||||
}
|
||||
|
||||
private void printMemoryCoreCaches(Collection<CacheInfo> caches) {
|
||||
for (CacheInfo cache : caches) {
|
||||
if (!cache.name.contains("-") && CacheType.MEM.equals(cache.type)) {
|
||||
printCache(cache);
|
||||
}
|
||||
CacheStats stat = cache.stats();
|
||||
stdout.print(String.format(
|
||||
" %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
|
||||
entry.getKey(),
|
||||
count(cache.size()),
|
||||
"",
|
||||
"",
|
||||
duration(stat.averageLoadPenalty()),
|
||||
percent(stat.hitCount(), stat.requestCount()),
|
||||
""));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Cache<?, ?>> sortedCoreCaches() {
|
||||
SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
|
||||
for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
|
||||
cacheMap.byPlugin("gerrit").entrySet()) {
|
||||
m.put(cacheNameOf("gerrit", entry.getKey()), entry.getValue().get());
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<String, Cache<?, ?>> sortedPluginCaches() {
|
||||
SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
|
||||
for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
|
||||
if (!"gerrit".equals(e.getPluginName())) {
|
||||
m.put(cacheNameOf(e.getPluginName(), e.getExportName()),
|
||||
e.getProvider().get());
|
||||
private void printMemoryPluginCaches(Collection<CacheInfo> caches) {
|
||||
for (CacheInfo cache : caches) {
|
||||
if (cache.name.contains("-") && CacheType.MEM.equals(cache.type)) {
|
||||
printCache(cache);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private void printDiskCaches(Collection<CacheInfo> caches) {
|
||||
for (CacheInfo cache : caches) {
|
||||
if (CacheType.DISK.equals(cache.type)) {
|
||||
printCache(cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printCache(CacheInfo cache) {
|
||||
stdout.print(String.format(
|
||||
"%1s %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
|
||||
CacheType.DISK.equals(cache.type) ? "D" : "",
|
||||
cache.name,
|
||||
nullToEmpty(cache.entries.mem),
|
||||
nullToEmpty(cache.entries.disk),
|
||||
Strings.nullToEmpty(cache.entries.space),
|
||||
Strings.nullToEmpty(cache.averageGet),
|
||||
formatAsPercent(cache.hitRatio.mem),
|
||||
formatAsPercent(cache.hitRatio.disk)
|
||||
));
|
||||
}
|
||||
|
||||
private static String nullToEmpty(Long l) {
|
||||
return l != null ? String.valueOf(l) : "";
|
||||
}
|
||||
|
||||
private static String formatAsPercent(Integer i) {
|
||||
return i != null ? String.valueOf(i) + "%" : "";
|
||||
}
|
||||
|
||||
private void memSummary() {
|
||||
@ -358,39 +364,4 @@ final class ShowCaches extends CacheCommand {
|
||||
}
|
||||
return String.format("%1$6.2f%2$s", value, suffix);
|
||||
}
|
||||
|
||||
private String count(long cnt) {
|
||||
if (cnt == 0) {
|
||||
return "";
|
||||
}
|
||||
return String.format("%6d", cnt);
|
||||
}
|
||||
|
||||
private String duration(double ns) {
|
||||
if (ns < 0.5) {
|
||||
return "";
|
||||
}
|
||||
String suffix = "ns";
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "us";
|
||||
}
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "ms";
|
||||
}
|
||||
if (ns >= 1000.0) {
|
||||
ns /= 1000.0;
|
||||
suffix = "s ";
|
||||
}
|
||||
return String.format("%4.1f%s", ns, suffix);
|
||||
}
|
||||
|
||||
private String percent(final long value, final long total) {
|
||||
if (total <= 0) {
|
||||
return "";
|
||||
}
|
||||
final long pcent = (100 * value) / total;
|
||||
return String.format("%3d%%", (int) pcent);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user