diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/Constants.java b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/Constants.java index b4ad3b645..f59e12bb9 100644 --- a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/Constants.java +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/Constants.java @@ -4,6 +4,7 @@ import one.nio.http.Response; import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Set; @@ -30,6 +31,18 @@ public final class Constants { public static final String HTTP_TIMESTAMP_HEADER = "X-Timestamp"; public static final String NIO_TIMESTAMP_HEADER = "x-timestamp:"; public static final String HTTP_TERMINATION_HEADER = "X-Termination"; + public static final String RANGE_REQUEST = "/v0/entities?start="; + public static final String ID_REQUEST = "/v0/entity?id="; + public static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] LAST_STRING = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] NEW_LINE = "\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] HEADER = + """ + HTTP/1.1 200 OK\r + Content-Type: text/plain\r + Transfer-Encoding: chunked\r + \r + """.getBytes(StandardCharsets.UTF_8); private Constants() { } diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/RequestHandler.java b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/RequestHandler.java index 72e4d9070..266a1ec8d 100644 --- a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/RequestHandler.java +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/RequestHandler.java @@ -10,6 +10,7 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.nio.charset.StandardCharsets; +import java.util.Iterator; public class RequestHandler { private final Dao dao; @@ -84,4 +85,11 @@ private Response sendResponseWithTimestamp(String resultCode, byte[] body, long private MemorySegment fromString(String data) { return data == null ? null : MemorySegment.ofArray(data.getBytes(StandardCharsets.UTF_8)); } + + public Iterator> getEntries(String start, String end) { + MemorySegment startVal = fromString(start); + MemorySegment endVal = end != null ? fromString(end) : null; + + return dao.get(startVal, endVal); + } } diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServerImpl.java b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServerImpl.java index ac0216c82..ba6243804 100644 --- a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServerImpl.java +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServerImpl.java @@ -11,8 +11,10 @@ import org.slf4j.LoggerFactory; import ru.vk.itmo.ServiceConfig; import ru.vk.itmo.test.tuzikovalexandr.dao.Dao; +import ru.vk.itmo.test.tuzikovalexandr.dao.EntryWithTimestamp; import java.io.IOException; +import java.lang.foreign.MemorySegment; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -20,6 +22,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -78,7 +81,16 @@ public void handleDefault(Request request, HttpSession session) throws IOExcepti @Override public void handleRequest(Request request, HttpSession session) throws IOException { try { - if (!request.getURI().startsWith("/v0/entity?id=") || !Constants.METHODS.contains(request.getMethod())) { + if (request.getURI().startsWith(Constants.RANGE_REQUEST)) { + String paramStart = request.getParameter("start="); + String paramEnd = request.getParameter("end="); + + rangeRequest(session, paramStart, paramEnd); + return; + } + + if (!request.getURI().startsWith(Constants.ID_REQUEST) + || !Constants.METHODS.contains(request.getMethod())) { handleDefault(request, session); return; } @@ -109,6 +121,25 @@ public void handleRequest(Request request, HttpSession session) throws IOExcepti } } + private void rangeRequest(HttpSession session, String start, String end) { + if (start == null || start.isBlank()) { + sendResponse(session, new Response(Response.BAD_REQUEST, Response.EMPTY)); + return; + } + + executorService.execute(() -> { + try { + Iterator> entries = requestHandler.getEntries(start, end); + + final StreamResponse streamResponse = new StreamResponse(Response.OK, entries); + streamResponse.stream(session); + } catch (IOException e) { + log.error("Exception while sending close connection", e); + session.scheduleClose(); + } + }); + } + private void processingRequest(Request request, HttpSession session, long processingStartTime, String paramId, int from, int ack) throws IOException { if (System.currentTimeMillis() - processingStartTime > Constants.REQUEST_TIMEOUT) { diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServiceImpl.java b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServiceImpl.java index 0415227bc..4913ecb63 100644 --- a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServiceImpl.java +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/ServiceImpl.java @@ -48,7 +48,7 @@ public synchronized CompletableFuture stop() throws IOException { return CompletableFuture.completedFuture(null); } - @ServiceFactory(stage = 5) + @ServiceFactory(stage = 6) public static class Factory implements ServiceFactory.Factory { @Override diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/StreamResponse.java b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/StreamResponse.java new file mode 100644 index 000000000..a9102c71d --- /dev/null +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/StreamResponse.java @@ -0,0 +1,74 @@ +package ru.vk.itmo.test.tuzikovalexandr; + +import one.nio.http.Response; +import one.nio.net.Session; +import ru.vk.itmo.test.tuzikovalexandr.dao.EntryWithTimestamp; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +public class StreamResponse extends Response { + private final Iterator> entries; + + public StreamResponse(String resultCode, Iterator> entries) { + super(resultCode); + this.entries = entries; + } + + public void stream(Session session) throws IOException { + session.write(Constants.HEADER, 0, Constants.HEADER.length); + + processWrite(session); + session.scheduleClose(); + } + + private void next(Session session, MemorySegment key, MemorySegment value) throws IOException { + byte[] keyInBytes = toByteArray(key); + byte[] valueInBytes = toByteArray(value); + + byte[] entrySize = Integer.toHexString(keyInBytes.length + valueInBytes.length + Constants.NEW_LINE.length) + .getBytes(StandardCharsets.UTF_8); + + byte[] resultValue = new byte[keyInBytes.length + valueInBytes.length + Constants.NEW_LINE.length + + Constants.CRLF.length * 2 + entrySize.length]; + + ByteBuffer target = ByteBuffer.wrap(resultValue); + target.put(entrySize); + target.put(Constants.CRLF); + target.put(keyInBytes); + target.put(Constants.NEW_LINE); + target.put(valueInBytes); + target.put(Constants.CRLF); + + session.write(target.array(), 0, target.array().length); + } + + private void processWrite(Session session) throws IOException { + while (entries.hasNext()) { + EntryWithTimestamp entry = entries.next(); + + next(session, entry.key(), entry.value()); + } + session.write(Constants.LAST_STRING, 0, Constants.LAST_STRING.length); + } + + private byte[] toByteArray(T segment) { + if (segment == null) { + throw new IllegalArgumentException(); + } + + if (segment instanceof MemorySegment) { + return ((MemorySegment) segment).toArray(ValueLayout.JAVA_BYTE); + } + + if (segment instanceof String) { + return ((String) segment).getBytes(StandardCharsets.UTF_8); + } + + return new byte[0]; + } +} diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-alloc.html b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-alloc.html new file mode 100644 index 000000000..d687f86d3 --- /dev/null +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-alloc.html @@ -0,0 +1,308 @@ + + + + + + + +

Flame Graph

+
  
+
Produced by async-profiler
+ +
+

+

Matched:

+ diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-cpu.html b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-cpu.html new file mode 100644 index 000000000..f43889a17 --- /dev/null +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-cpu.html @@ -0,0 +1,625 @@ + + + + + + + +

Flame Graph

+
  
+
Produced by async-profiler
+ +
+

+

Matched:

+ diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-lock.html b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-lock.html new file mode 100644 index 000000000..24b7ef49f --- /dev/null +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/profile-lock.html @@ -0,0 +1,260 @@ + + + + + + + +

Flame Graph

+
  
+
Produced by async-profiler
+ +
+

+

Matched:

+ diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/report.md b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/report.md new file mode 100644 index 000000000..6b92d9b10 --- /dev/null +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/results/stage6/report.md @@ -0,0 +1,50 @@ +## Начальные сведения +* Виртуалке были даны 2 ядра (влиять это ни на что не должно, кроме заполнения базы) +* Остальное все по умолчанию + +## CPU + +[profile-cpu.html](profile-cpu.html) + +- RequestHandler.getEntries 1%. Очень быстрая работа dao. Возможно потому что все-таки данных в базе было мало. +- StreamResponse.stream 89,69%. Метод работы с чанками и запись их в сессию (сокет). + +В методе StreamResponse.stream большую часть занимает запись в сессию, и лишь малую часть на конвертацию данных +в массив байт: +- Integer.toHexString 2%; +- toByteArray 5% +- Session.write 76%. + +Метод StreamResponse.processWrite 87% тут же и работа итератора. + +## ALLOC + +[profile-alloc.html](profile-alloc.html) + +На профиле видны все аллокации, которые создаются при формировании чанков, а именно: +- Integer.toHexString при переводе длинны сообщения в строку; +- String.getBytes при переводе строки в массив байт; +- Session.write когда происходит запись в сессию; +- toArray при переводе MemorySegment в массив байт. + +На профиле нет аллокации ByteBuffer. Но, как я понимаю, его и не должно быть. +Метод ByteBuffer.wrap() не выделяет новую память для хранения данных. +Он создает новый объект ByteBuffer, который обертывает существующий массив байтов, +переданный ему в качестве аргумента. +Таким образом, данный метод не производит аллокацию памяти. + +## LOCK + +[profile-lock.html](profile-lock.html) + +Интересно, что профиль блокировок получился пустым. + +## Выводы + +- При нынешней реализации формируется большой блок данных, а не отправляется сразу (как я понимаю); +- Также при нынешней реализации существуют лишние аллокции, такие как Integer.toHexString, String.getBytes. + +## Улучшения + +- Возможно не переводить MemorySegment в массив байт, а работать сразу с ними; +- Ассинхронная запись данных в сокет. \ No newline at end of file diff --git a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/scripts/putRequests.lua b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/scripts/putRequests.lua index a3873f04f..19b38263f 100644 --- a/src/main/java/ru/vk/itmo/test/tuzikovalexandr/scripts/putRequests.lua +++ b/src/main/java/ru/vk/itmo/test/tuzikovalexandr/scripts/putRequests.lua @@ -1,10 +1,10 @@ id = 0 function request() id = id + 1 - key = "k" .. id + key = "k" .. math.random(10000000) path = "/v0/entity?id=" .. key - value = "v" .. math.random(100000) + value = "v" .. math.random(10000000) --headers = { } --headers["Host"] = "localhost:8080"