JavaFX thread is hanging when using ExecutorService
I'm trying to write a program that uses Imgur's API to download images based on an account name.
private volatile int threadCount;
private volatile double totalFileSize;
private volatile List<String> albums = new ArrayList<>();
private volatile Map<JSONObject, String> images = new HashMap<>();
private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
return t;
});
private void downloadAlbums(List<String> albums) {
threadCount = 0;
albums.forEach((albumHash) -> {
if (hasRemainingRequests()) {
incThreadCount();
executorService.execute(() -> {
try {
String responseString;
String dirTitle;
String albumUrl = URL_ALBUM + albumHash;
String query = String.format("client_id=%s", URLEncoder.encode(CLIENT_ID, CHARSET));
URLConnection connection = new URL(albumUrl + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", CHARSET);
InputStream response = connection.getInputStream();
try (Scanner scanner = new Scanner(response)) {
responseString = scanner.useDelimiter("\A").next();
JSONObject obj = new JSONObject(responseString).getJSONObject("data");
dirTitle = obj.getString("title");
String temp = "";
// Get save path from a TextField somewhere else on the GUI
ObservableList<Node> nodes = primaryStage.getScene().getRoot().getChildrenUnmodifiable();
for (Node node : nodes) {
if (node instanceof VBox) {
ObservableList<Node> vNodes = ((VBox) node).getChildrenUnmodifiable();
for (Node vNode : vNodes) {
if (vNode instanceof DestinationBrowser) {
temp = ((DestinationBrowser) vNode).getDestination().trim();
}
}
}
}
final String path = temp + "\" + formatPath(accountName) + "\" + formatPath(dirTitle);
JSONArray arr = obj.getJSONArray("images");
arr.forEach((jsonObject) -> {
totalFileSize += ((JSONObject) jsonObject).getDouble("size");
images.put((JSONObject) jsonObject, path);
});
}
} catch (IOException ex) {
//
} catch (Exception ex) {
//
} finally {
decThreadCount();
if (threadCount == 0) {
Platform.runLater(() -> {
DecimalFormat df = new DecimalFormat("#.#");
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);// 714833218
alert.setHeaderText("Found " + images.size() + " images (" + (totalFileSize < 1000000000 ? df.format(totalFileSize / 1000000) + " MB)" : df.format(totalFileSize / 1000000000) + " GB)"));
alert.setContentText("Proceed with download and save images?");
Optional<ButtonType> alertResponse = alert.showAndWait();
if (alertResponse.get() == ButtonType.OK) {
progressBar.setTotalWork(images.size());
executorService.execute(() -> {
for (JSONObject obj : images.keySet()) {
(new File(images.get(obj))).mkdirs();
downloadImage(obj, images.get(obj));
}
});
}
});
}
}
});
}
});
}
albums is a List of codes that are required to send a GET request to Imgur in order to receive that album's images. The data returned is then used in another method which downloads the images themselves.
Now, all of this works fine, but when the program is making all the GET requests the JavaFX thread hangs (GUI becomes unresponsive). And after all the GET requests have been executed, the JavaFX thread stops hanging and the alert shows up with the correct information.
I just don't understand why the GUI becomes unresponsive when I'm not (I believe I'm not) blocking it's thread and I'm using an ExecutorService to do all the network requests.
java multithreading javafx executorservice
add a comment |
I'm trying to write a program that uses Imgur's API to download images based on an account name.
private volatile int threadCount;
private volatile double totalFileSize;
private volatile List<String> albums = new ArrayList<>();
private volatile Map<JSONObject, String> images = new HashMap<>();
private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
return t;
});
private void downloadAlbums(List<String> albums) {
threadCount = 0;
albums.forEach((albumHash) -> {
if (hasRemainingRequests()) {
incThreadCount();
executorService.execute(() -> {
try {
String responseString;
String dirTitle;
String albumUrl = URL_ALBUM + albumHash;
String query = String.format("client_id=%s", URLEncoder.encode(CLIENT_ID, CHARSET));
URLConnection connection = new URL(albumUrl + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", CHARSET);
InputStream response = connection.getInputStream();
try (Scanner scanner = new Scanner(response)) {
responseString = scanner.useDelimiter("\A").next();
JSONObject obj = new JSONObject(responseString).getJSONObject("data");
dirTitle = obj.getString("title");
String temp = "";
// Get save path from a TextField somewhere else on the GUI
ObservableList<Node> nodes = primaryStage.getScene().getRoot().getChildrenUnmodifiable();
for (Node node : nodes) {
if (node instanceof VBox) {
ObservableList<Node> vNodes = ((VBox) node).getChildrenUnmodifiable();
for (Node vNode : vNodes) {
if (vNode instanceof DestinationBrowser) {
temp = ((DestinationBrowser) vNode).getDestination().trim();
}
}
}
}
final String path = temp + "\" + formatPath(accountName) + "\" + formatPath(dirTitle);
JSONArray arr = obj.getJSONArray("images");
arr.forEach((jsonObject) -> {
totalFileSize += ((JSONObject) jsonObject).getDouble("size");
images.put((JSONObject) jsonObject, path);
});
}
} catch (IOException ex) {
//
} catch (Exception ex) {
//
} finally {
decThreadCount();
if (threadCount == 0) {
Platform.runLater(() -> {
DecimalFormat df = new DecimalFormat("#.#");
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);// 714833218
alert.setHeaderText("Found " + images.size() + " images (" + (totalFileSize < 1000000000 ? df.format(totalFileSize / 1000000) + " MB)" : df.format(totalFileSize / 1000000000) + " GB)"));
alert.setContentText("Proceed with download and save images?");
Optional<ButtonType> alertResponse = alert.showAndWait();
if (alertResponse.get() == ButtonType.OK) {
progressBar.setTotalWork(images.size());
executorService.execute(() -> {
for (JSONObject obj : images.keySet()) {
(new File(images.get(obj))).mkdirs();
downloadImage(obj, images.get(obj));
}
});
}
});
}
}
});
}
});
}
albums is a List of codes that are required to send a GET request to Imgur in order to receive that album's images. The data returned is then used in another method which downloads the images themselves.
Now, all of this works fine, but when the program is making all the GET requests the JavaFX thread hangs (GUI becomes unresponsive). And after all the GET requests have been executed, the JavaFX thread stops hanging and the alert shows up with the correct information.
I just don't understand why the GUI becomes unresponsive when I'm not (I believe I'm not) blocking it's thread and I'm using an ExecutorService to do all the network requests.
java multithreading javafx executorservice
2
How ishasRemainingRequests()implemented? BTW: there are several issues with your implementation: Makingimagesvolatileonly makes sure a replacement of the reference is visible to other threads. There's no guarantee that theHashMapitself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).
– fabian
Nov 21 '18 at 16:25
incThreadCount()/decThreadCount()could also be an issue and I recommend usingAtomicInteger
– fabian
Nov 21 '18 at 16:26
@fabianhasRemainingRequests()sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.
– GodsWithin
Nov 21 '18 at 16:44
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01
add a comment |
I'm trying to write a program that uses Imgur's API to download images based on an account name.
private volatile int threadCount;
private volatile double totalFileSize;
private volatile List<String> albums = new ArrayList<>();
private volatile Map<JSONObject, String> images = new HashMap<>();
private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
return t;
});
private void downloadAlbums(List<String> albums) {
threadCount = 0;
albums.forEach((albumHash) -> {
if (hasRemainingRequests()) {
incThreadCount();
executorService.execute(() -> {
try {
String responseString;
String dirTitle;
String albumUrl = URL_ALBUM + albumHash;
String query = String.format("client_id=%s", URLEncoder.encode(CLIENT_ID, CHARSET));
URLConnection connection = new URL(albumUrl + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", CHARSET);
InputStream response = connection.getInputStream();
try (Scanner scanner = new Scanner(response)) {
responseString = scanner.useDelimiter("\A").next();
JSONObject obj = new JSONObject(responseString).getJSONObject("data");
dirTitle = obj.getString("title");
String temp = "";
// Get save path from a TextField somewhere else on the GUI
ObservableList<Node> nodes = primaryStage.getScene().getRoot().getChildrenUnmodifiable();
for (Node node : nodes) {
if (node instanceof VBox) {
ObservableList<Node> vNodes = ((VBox) node).getChildrenUnmodifiable();
for (Node vNode : vNodes) {
if (vNode instanceof DestinationBrowser) {
temp = ((DestinationBrowser) vNode).getDestination().trim();
}
}
}
}
final String path = temp + "\" + formatPath(accountName) + "\" + formatPath(dirTitle);
JSONArray arr = obj.getJSONArray("images");
arr.forEach((jsonObject) -> {
totalFileSize += ((JSONObject) jsonObject).getDouble("size");
images.put((JSONObject) jsonObject, path);
});
}
} catch (IOException ex) {
//
} catch (Exception ex) {
//
} finally {
decThreadCount();
if (threadCount == 0) {
Platform.runLater(() -> {
DecimalFormat df = new DecimalFormat("#.#");
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);// 714833218
alert.setHeaderText("Found " + images.size() + " images (" + (totalFileSize < 1000000000 ? df.format(totalFileSize / 1000000) + " MB)" : df.format(totalFileSize / 1000000000) + " GB)"));
alert.setContentText("Proceed with download and save images?");
Optional<ButtonType> alertResponse = alert.showAndWait();
if (alertResponse.get() == ButtonType.OK) {
progressBar.setTotalWork(images.size());
executorService.execute(() -> {
for (JSONObject obj : images.keySet()) {
(new File(images.get(obj))).mkdirs();
downloadImage(obj, images.get(obj));
}
});
}
});
}
}
});
}
});
}
albums is a List of codes that are required to send a GET request to Imgur in order to receive that album's images. The data returned is then used in another method which downloads the images themselves.
Now, all of this works fine, but when the program is making all the GET requests the JavaFX thread hangs (GUI becomes unresponsive). And after all the GET requests have been executed, the JavaFX thread stops hanging and the alert shows up with the correct information.
I just don't understand why the GUI becomes unresponsive when I'm not (I believe I'm not) blocking it's thread and I'm using an ExecutorService to do all the network requests.
java multithreading javafx executorservice
I'm trying to write a program that uses Imgur's API to download images based on an account name.
private volatile int threadCount;
private volatile double totalFileSize;
private volatile List<String> albums = new ArrayList<>();
private volatile Map<JSONObject, String> images = new HashMap<>();
private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
return t;
});
private void downloadAlbums(List<String> albums) {
threadCount = 0;
albums.forEach((albumHash) -> {
if (hasRemainingRequests()) {
incThreadCount();
executorService.execute(() -> {
try {
String responseString;
String dirTitle;
String albumUrl = URL_ALBUM + albumHash;
String query = String.format("client_id=%s", URLEncoder.encode(CLIENT_ID, CHARSET));
URLConnection connection = new URL(albumUrl + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", CHARSET);
InputStream response = connection.getInputStream();
try (Scanner scanner = new Scanner(response)) {
responseString = scanner.useDelimiter("\A").next();
JSONObject obj = new JSONObject(responseString).getJSONObject("data");
dirTitle = obj.getString("title");
String temp = "";
// Get save path from a TextField somewhere else on the GUI
ObservableList<Node> nodes = primaryStage.getScene().getRoot().getChildrenUnmodifiable();
for (Node node : nodes) {
if (node instanceof VBox) {
ObservableList<Node> vNodes = ((VBox) node).getChildrenUnmodifiable();
for (Node vNode : vNodes) {
if (vNode instanceof DestinationBrowser) {
temp = ((DestinationBrowser) vNode).getDestination().trim();
}
}
}
}
final String path = temp + "\" + formatPath(accountName) + "\" + formatPath(dirTitle);
JSONArray arr = obj.getJSONArray("images");
arr.forEach((jsonObject) -> {
totalFileSize += ((JSONObject) jsonObject).getDouble("size");
images.put((JSONObject) jsonObject, path);
});
}
} catch (IOException ex) {
//
} catch (Exception ex) {
//
} finally {
decThreadCount();
if (threadCount == 0) {
Platform.runLater(() -> {
DecimalFormat df = new DecimalFormat("#.#");
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);// 714833218
alert.setHeaderText("Found " + images.size() + " images (" + (totalFileSize < 1000000000 ? df.format(totalFileSize / 1000000) + " MB)" : df.format(totalFileSize / 1000000000) + " GB)"));
alert.setContentText("Proceed with download and save images?");
Optional<ButtonType> alertResponse = alert.showAndWait();
if (alertResponse.get() == ButtonType.OK) {
progressBar.setTotalWork(images.size());
executorService.execute(() -> {
for (JSONObject obj : images.keySet()) {
(new File(images.get(obj))).mkdirs();
downloadImage(obj, images.get(obj));
}
});
}
});
}
}
});
}
});
}
albums is a List of codes that are required to send a GET request to Imgur in order to receive that album's images. The data returned is then used in another method which downloads the images themselves.
Now, all of this works fine, but when the program is making all the GET requests the JavaFX thread hangs (GUI becomes unresponsive). And after all the GET requests have been executed, the JavaFX thread stops hanging and the alert shows up with the correct information.
I just don't understand why the GUI becomes unresponsive when I'm not (I believe I'm not) blocking it's thread and I'm using an ExecutorService to do all the network requests.
java multithreading javafx executorservice
java multithreading javafx executorservice
asked Nov 21 '18 at 15:28
GodsWithin
61
61
2
How ishasRemainingRequests()implemented? BTW: there are several issues with your implementation: Makingimagesvolatileonly makes sure a replacement of the reference is visible to other threads. There's no guarantee that theHashMapitself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).
– fabian
Nov 21 '18 at 16:25
incThreadCount()/decThreadCount()could also be an issue and I recommend usingAtomicInteger
– fabian
Nov 21 '18 at 16:26
@fabianhasRemainingRequests()sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.
– GodsWithin
Nov 21 '18 at 16:44
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01
add a comment |
2
How ishasRemainingRequests()implemented? BTW: there are several issues with your implementation: Makingimagesvolatileonly makes sure a replacement of the reference is visible to other threads. There's no guarantee that theHashMapitself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).
– fabian
Nov 21 '18 at 16:25
incThreadCount()/decThreadCount()could also be an issue and I recommend usingAtomicInteger
– fabian
Nov 21 '18 at 16:26
@fabianhasRemainingRequests()sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.
– GodsWithin
Nov 21 '18 at 16:44
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01
2
2
How is
hasRemainingRequests() implemented? BTW: there are several issues with your implementation: Making images volatile only makes sure a replacement of the reference is visible to other threads. There's no guarantee that the HashMap itself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).– fabian
Nov 21 '18 at 16:25
How is
hasRemainingRequests() implemented? BTW: there are several issues with your implementation: Making images volatile only makes sure a replacement of the reference is visible to other threads. There's no guarantee that the HashMap itself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).– fabian
Nov 21 '18 at 16:25
incThreadCount()/decThreadCount() could also be an issue and I recommend using AtomicInteger– fabian
Nov 21 '18 at 16:26
incThreadCount()/decThreadCount() could also be an issue and I recommend using AtomicInteger– fabian
Nov 21 '18 at 16:26
@fabian
hasRemainingRequests() sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.– GodsWithin
Nov 21 '18 at 16:44
@fabian
hasRemainingRequests() sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.– GodsWithin
Nov 21 '18 at 16:44
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01
add a comment |
0
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53415361%2fjavafx-thread-is-hanging-when-using-executorservice%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
0
active
oldest
votes
0
active
oldest
votes
active
oldest
votes
active
oldest
votes
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53415361%2fjavafx-thread-is-hanging-when-using-executorservice%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
How is
hasRemainingRequests()implemented? BTW: there are several issues with your implementation: Makingimagesvolatileonly makes sure a replacement of the reference is visible to other threads. There's no guarantee that theHashMapitself is consistent for the threads. Also you read from the the GUI on background threads. Accessing the GUI like this can lead to unexpected results including exceptions. Furthermore 100 is a huge number of threads, especially since they seem to access the same resource (your internet connection).– fabian
Nov 21 '18 at 16:25
incThreadCount()/decThreadCount()could also be an issue and I recommend usingAtomicInteger– fabian
Nov 21 '18 at 16:26
@fabian
hasRemainingRequests()sends a requests to the Imgur API, who responds with how many requests the user (IP based) and the client (CLIENT_ID based) are still allowed to make before access to the API is blocked.– GodsWithin
Nov 21 '18 at 16:44
100 threads is indeed a huge number. I wouldn’t be surprised if you are simply starving the JavaFX application thread of CPU time.
– VGR
Nov 21 '18 at 17:21
@fabian I tested this with lower thread counts as well (4-8), but the hanging kept occuring.
– GodsWithin
Nov 21 '18 at 18:01