Make connection to CodeGeneratorService lazy. #509
Make connection to CodeGeneratorService lazy. #509
Conversation
|
||
import com.google.blockly.android.codegen.CodeGeneratorService.CodeGeneratorBinder; | ||
|
||
class LazyServiceConnection { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JavaDoc
Maybe rename class as LazyServiceWrapper (your call).
if (isBound()) { | ||
executeCodeGenerationRequest(codeGenerationRequest); | ||
} else { | ||
mStoredRequest = codeGenerationRequest; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there is a risk of overwriting a prior value of mStoredRequest. To avoid dropping multiple requests, mStoredRequests probably needs to be a queue. That queue may need to be copied to the Service object.
(That said, our callback does not have a mechanism for differentiating multiple requests is the same callback function is used.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is an edge case, but I've added a queue anyway.
If you use different callback objects with different code generation requests then this all works as expected.
codeGenerationCallback, | ||
blockDefinitionsJsonPaths, | ||
generatorsJsPaths); | ||
mCodeGenerationConnection.requestCodeGeneration(codeGenerationRequest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revisiting this code, the new CodeGenerationRequest() feels odd here, in that it doesn't seem to provide value to this class.
Option #1: It could be passed constructed previously, and then passed in. This eliminates the need for workspace in this class, and could be returned as part of the callback. I prefer this for that last point, but that would be a breaking change to the callback interface.
Option #2: The CodeGenerationRequest could be buried deeper, passing each of the parameters into the lazy service for construction there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with 1, and discovered that it removes all the logic from CodeGenerationManager, so I've done that, then relocated the lazy loading into CodeGenerationManager and deleted the new LazyServiceConnection class.
public void onServiceConnected(ComponentName className, IBinder binder) { | ||
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService(); | ||
while (!mStoredRequests.isEmpty()) { | ||
executeCodeGenerationRequest(mStoredRequests.poll()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used poll()
rather than remove()
here, as the private execute method can handle nulls.
public void onServiceConnected(ComponentName className, IBinder binder) { | ||
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService(); | ||
while (!mStoredRequests.isEmpty()) { | ||
executeCodeGenerationRequest(mStoredRequests.poll()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used poll()
rather than remove()
here, as executeCodeGenerationRequest
can handle nulls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're aiming to resolve #37, it is the bindService() that should be lazy. Sorry if the issue wasn't clear, and that I didn't catch this in the previous review.
public void onResume() { | ||
Intent intent = new Intent(mContext, CodeGeneratorService.class); | ||
mContext.bindService(intent, mCodeGenerationConnection, Context.BIND_AUTO_CREATE); | ||
if (mCodeGenerationConnection != null) { | ||
mContext.bindService(intent, mCodeGenerationConnection, Context.BIND_AUTO_CREATE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, it is this bind that could be lazy, and is the point of issue #37. We should wait until the first requestCodeGeneration()
call, since it is entirely possible Blockly is used without code generation, and a full background WebView is fairly heavyweight. Scratch Blocks is an example of this sort of usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. I've made an update so that the bind is lazy (rather than creation of the connection).
I think there's potentially a risk now that bind gets called multiple times if the binding hasn't finished before more code generation is requested. I'm not sure how the context will handle multiple binds of the same object. It might be a non-issue. Update: From reading the docs, I think bind service is synchronous as it returns a boolean which represents if the bind was successful or not. Therefore I don't think the above is a problem. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some notes about how to protect against race conditions and leftover requests. In practice, the service binding should be happening immediately since it's a local service, but it's good to add these if you're making changes to the service binding.
public void onPause() { | ||
mContext.unbindService(mCodeGenerationConnection); | ||
if (isBound()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to deal with race conditions, including the user leaving and re-entering while a bind request is in progress, you'll want to keep track of the desired state.
- In onPause you should also set a mResumed flag to false (and set it to true in onResume).
- In onServiceConnected check if (mResumed). If not, disconnected immediately.
- In onResume you should clear out any pending requests since you won't reconnect until a new request comes in.
Log.i(TAG, "Generator not bound to service. Skipping run request."); | ||
} | ||
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) { | ||
if (isBound()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add if(!mResumed) { Log.w(TAG, "Code generation called while paused. Request ignored."); return;}
} | ||
} | ||
|
||
private boolean isBound() { | ||
return mGeneratorService != null; | ||
} | ||
|
||
private void connectToService() { | ||
Intent intent = new Intent(mContext, CodeGeneratorService.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a mIsConnecting flag. Set it to true after bindService is called and false when the service connects. In this method check if (!isBound() && !mIsConnecting) before calling bindService.
I've updated to handle these cases. Thank you both for your detailed feedback! |
Review status: 0 of 2 files reviewed at latest revision, 11 unresolved discussions. blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 40 at r4 (raw file):
nit line length blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 41 at r4 (raw file):
move mIsConnection = false to the top of the method so it's always done. blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 89 at r4 (raw file):
move this check into connectToService so the caller doesn't have to worry about it. Comments from Reviewable |
} | ||
} else { | ||
Log.i(TAG, "Generator not bound to service. Skipping run request."); | ||
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NotNull CodeGenerationRequest codeGenerationRequest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nulls are already handled in the code, but I'll add a null check here so that that's clearer.
} else { | ||
Log.i(TAG, "Generator not bound to service. Skipping run request."); | ||
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) { | ||
if (isBound()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check resumed state first.
mContext.unbindService(mCodeGenerationConnection); | ||
} else { | ||
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService(); | ||
mIsConnecting = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move mIsConnecting = false;
to the end, outside of the if (!mResumed)
check. Maybe even in a finally
block.
} | ||
} else { | ||
Log.i(TAG, "No blocks in workspace. Skipping run request."); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is going to have merge conflicts with some upcoming changes, I've updated the PR to target the toolbox-refactor
branch. I'd appreciate if I can have write access to this branch to fix the upcoming conflict.
Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've done this, but let me know if there are still issues (the merge conflicts were all imports, so trivial to resolve).
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
Fixes #37
This change is